From 8abb4e036c57a0aa879ba4b6188d3bc4a2a4ba3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Tue, 9 May 2017 17:03:32 +0000 Subject: [PATCH 001/110] Initial empty repository From 9d409803b62069791278200ce809cc05a43f7473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Wed, 10 May 2017 17:27:57 +0200 Subject: [PATCH 002/110] Adds basic, advanced and CP samples, with placeholder README Change-Id: I9a444a57d14f20bdfa7c9b395c210e3248df2377 --- .gitignore | 8 + CONTRIBUTING.md | 35 +++ GithubBrowserSample/.gitignore | 8 + LICENSE | 201 +++++++++++++++++ PersistenceBasicSample/.gitignore | 8 + PersistenceBasicSample/CONTRIBUTING.md | 35 +++ PersistenceBasicSample/LICENSE | 201 +++++++++++++++++ PersistenceBasicSample/app/build.gradle | 76 +++++++ PersistenceBasicSample/app/proguard-rules.pro | 17 ++ .../android/persistence/MainActivityTest.java | 132 +++++++++++ .../app/src/main/AndroidManifest.xml | 40 ++++ .../android/persistence/MainActivity.java | 51 +++++ .../android/persistence/ProductFragment.java | 112 +++++++++ .../persistence/ProductListFragment.java | 92 ++++++++ .../android/persistence/db/AppDatabase.java | 38 ++++ .../persistence/db/DatabaseCreator.java | 123 ++++++++++ .../persistence/db/DatabaseInitUtil.java | 88 ++++++++ .../db/converter/DateConverter.java | 33 +++ .../persistence/db/dao/CommentDao.java | 40 ++++ .../persistence/db/dao/ProductDao.java | 42 ++++ .../persistence/db/entity/CommentEntity.java | 86 +++++++ .../persistence/db/entity/ProductEntity.java | 77 +++++++ .../android/persistence/model/Comment.java | 26 +++ .../android/persistence/model/Product.java | 24 ++ .../persistence/ui/BindingAdapters.java | 28 +++ .../persistence/ui/CommentAdapter.java | 111 +++++++++ .../persistence/ui/CommentClickCallback.java | 23 ++ .../persistence/ui/ProductAdapter.java | 110 +++++++++ .../persistence/ui/ProductClickCallback.java | 23 ++ .../viewmodel/ProductListViewModel.java | 70 ++++++ .../viewmodel/ProductViewModel.java | 134 +++++++++++ .../app/src/main/res/layout/comment_item.xml | 46 ++++ .../app/src/main/res/layout/list_fragment.xml | 51 +++++ .../app/src/main/res/layout/main_activity.xml | 23 ++ .../src/main/res/layout/product_fragment.xml | 72 ++++++ .../app/src/main/res/layout/product_item.xml | 65 ++++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3118 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1773 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3618 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6209 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9479 bytes .../app/src/main/res/values/colors.xml | 24 ++ .../app/src/main/res/values/dimens.xml | 25 ++ .../app/src/main/res/values/product_app.xml | 21 ++ .../app/src/main/res/values/strings.xml | 25 ++ .../app/src/main/res/values/styles.xml | 27 +++ PersistenceBasicSample/build.gradle | 52 +++++ PersistenceBasicSample/gradle.properties | 33 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 22 ++ PersistenceBasicSample/gradlew | 160 +++++++++++++ PersistenceBasicSample/gradlew.bat | 90 ++++++++ PersistenceBasicSample/settings.gradle | 17 ++ PersistenceContentProviderSample/.gitignore | 9 + .../app/.gitignore | 1 + .../app/build.gradle | 63 ++++++ .../app/proguard-rules.pro | 25 ++ .../1.json | 46 ++++ .../contentprovidersample/CheeseTest.java | 64 ++++++ .../SampleContentProviderTest.java | 162 +++++++++++++ .../app/src/main/AndroidManifest.xml | 44 ++++ .../contentprovidersample/MainActivity.java | 137 +++++++++++ .../contentprovidersample/data/Cheese.java | 201 +++++++++++++++++ .../contentprovidersample/data/CheeseDao.java | 93 ++++++++ .../data/SampleDatabase.java | 87 +++++++ .../provider/SampleContentProvider.java | 213 ++++++++++++++++++ .../app/src/main/res/layout/main_activity.xml | 24 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3284 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2051 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4145 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6564 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9340 bytes .../app/src/main/res/values/colors.xml | 18 ++ .../app/src/main/res/values/strings.xml | 16 ++ .../app/src/main/res/values/styles.xml | 22 ++ PersistenceContentProviderSample/build.gradle | 40 ++++ .../gradle.properties | 17 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + PersistenceContentProviderSample/gradlew | 160 +++++++++++++ PersistenceContentProviderSample/gradlew.bat | 90 ++++++++ .../settings.gradle | 1 + README.md | 46 ++++ 83 files changed, 4430 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 GithubBrowserSample/.gitignore create mode 100644 LICENSE create mode 100644 PersistenceBasicSample/.gitignore create mode 100644 PersistenceBasicSample/CONTRIBUTING.md create mode 100644 PersistenceBasicSample/LICENSE create mode 100644 PersistenceBasicSample/app/build.gradle create mode 100644 PersistenceBasicSample/app/proguard-rules.pro create mode 100644 PersistenceBasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java create mode 100644 PersistenceBasicSample/app/src/main/AndroidManifest.xml create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Product.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java create mode 100644 PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java create mode 100644 PersistenceBasicSample/app/src/main/res/layout/comment_item.xml create mode 100644 PersistenceBasicSample/app/src/main/res/layout/list_fragment.xml create mode 100644 PersistenceBasicSample/app/src/main/res/layout/main_activity.xml create mode 100644 PersistenceBasicSample/app/src/main/res/layout/product_fragment.xml create mode 100644 PersistenceBasicSample/app/src/main/res/layout/product_item.xml create mode 100644 PersistenceBasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 PersistenceBasicSample/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 PersistenceBasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 PersistenceBasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 PersistenceBasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 PersistenceBasicSample/app/src/main/res/values/colors.xml create mode 100644 PersistenceBasicSample/app/src/main/res/values/dimens.xml create mode 100644 PersistenceBasicSample/app/src/main/res/values/product_app.xml create mode 100644 PersistenceBasicSample/app/src/main/res/values/strings.xml create mode 100644 PersistenceBasicSample/app/src/main/res/values/styles.xml create mode 100644 PersistenceBasicSample/build.gradle create mode 100644 PersistenceBasicSample/gradle.properties create mode 100644 PersistenceBasicSample/gradle/wrapper/gradle-wrapper.jar create mode 100644 PersistenceBasicSample/gradle/wrapper/gradle-wrapper.properties create mode 100755 PersistenceBasicSample/gradlew create mode 100644 PersistenceBasicSample/gradlew.bat create mode 100644 PersistenceBasicSample/settings.gradle create mode 100644 PersistenceContentProviderSample/.gitignore create mode 100644 PersistenceContentProviderSample/app/.gitignore create mode 100644 PersistenceContentProviderSample/app/build.gradle create mode 100644 PersistenceContentProviderSample/app/proguard-rules.pro create mode 100644 PersistenceContentProviderSample/app/schemas/com.example.android.contentprovidersample.data.SampleDatabase/1.json create mode 100644 PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/CheeseTest.java create mode 100644 PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/SampleContentProviderTest.java create mode 100644 PersistenceContentProviderSample/app/src/main/AndroidManifest.xml create mode 100644 PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/MainActivity.java create mode 100644 PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/Cheese.java create mode 100644 PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/CheeseDao.java create mode 100644 PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/SampleDatabase.java create mode 100644 PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/provider/SampleContentProvider.java create mode 100644 PersistenceContentProviderSample/app/src/main/res/layout/main_activity.xml create mode 100644 PersistenceContentProviderSample/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 PersistenceContentProviderSample/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 PersistenceContentProviderSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 PersistenceContentProviderSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 PersistenceContentProviderSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 PersistenceContentProviderSample/app/src/main/res/values/colors.xml create mode 100644 PersistenceContentProviderSample/app/src/main/res/values/strings.xml create mode 100644 PersistenceContentProviderSample/app/src/main/res/values/styles.xml create mode 100644 PersistenceContentProviderSample/build.gradle create mode 100644 PersistenceContentProviderSample/gradle.properties create mode 100644 PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.jar create mode 100644 PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties create mode 100755 PersistenceContentProviderSample/gradlew create mode 100644 PersistenceContentProviderSample/gradlew.bat create mode 100644 PersistenceContentProviderSample/settings.gradle create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f4b27534b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.idea +.gradle +/local.properties +.DS_Store +build/ +/captures +.externalNativeBuild diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..7b86f95d7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement (CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://cla.developers.google.com). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://cla.developers.google.com). + * Please make sure you sign both, Android and Google CLA + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Android Code Style Guide] + (https://source.android.com/source/code-style.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. diff --git a/GithubBrowserSample/.gitignore b/GithubBrowserSample/.gitignore new file mode 100644 index 000000000..f4b27534b --- /dev/null +++ b/GithubBrowserSample/.gitignore @@ -0,0 +1,8 @@ +*.iml +.idea +.gradle +/local.properties +.DS_Store +build/ +/captures +.externalNativeBuild diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..1af981f55 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/PersistenceBasicSample/.gitignore b/PersistenceBasicSample/.gitignore new file mode 100644 index 000000000..f4b27534b --- /dev/null +++ b/PersistenceBasicSample/.gitignore @@ -0,0 +1,8 @@ +*.iml +.idea +.gradle +/local.properties +.DS_Store +build/ +/captures +.externalNativeBuild diff --git a/PersistenceBasicSample/CONTRIBUTING.md b/PersistenceBasicSample/CONTRIBUTING.md new file mode 100644 index 000000000..7b86f95d7 --- /dev/null +++ b/PersistenceBasicSample/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement (CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://cla.developers.google.com). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://cla.developers.google.com). + * Please make sure you sign both, Android and Google CLA + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Android Code Style Guide] + (https://source.android.com/source/code-style.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. diff --git a/PersistenceBasicSample/LICENSE b/PersistenceBasicSample/LICENSE new file mode 100644 index 000000000..1af981f55 --- /dev/null +++ b/PersistenceBasicSample/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/PersistenceBasicSample/app/build.gradle b/PersistenceBasicSample/app/build.gradle new file mode 100644 index 000000000..79eb18434 --- /dev/null +++ b/PersistenceBasicSample/app/build.gradle @@ -0,0 +1,76 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion rootProject.buildToolsVersion + defaultConfig { + applicationId 'com.example.android.persistence' + minSdkVersion 21 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + dataBinding { + enabled = true + } + productFlavors { + } + + lintOptions { + abortOnError false + } + +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:appcompat-v7:' + rootProject.supportLibVersion; + compile 'com.android.support:cardview-v7:' + rootProject.supportLibVersion; + compile 'com.android.support:recyclerview-v7:' + rootProject.supportLibVersion; + compile 'android.arch.lifecycle:extensions:' + rootProject.archLifecycleVersion; + compile 'android.arch.persistence.room:runtime:' + rootProject.archRoomVersion; + annotationProcessor "android.arch.lifecycle:compiler:" + rootProject.archLifecycleVersion; + annotationProcessor "android.arch.persistence.room:compiler:" + rootProject.archRoomVersion; + + testCompile 'junit:junit:4.12' + + // Testing-only dependencies + androidTestCompile 'com.android.support.test:runner:' + rootProject.runnerVersion; + androidTestCompile 'com.android.support.test:rules:' + rootProject.rulesVersion; + androidTestCompile 'com.android.support.test.espresso:espresso-core:' + rootProject.espressoVersion; + + androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2'){ + exclude group: 'com.android.support', module: 'appcompat-v7' + exclude group: 'com.android.support', module: 'support-v4' + exclude module: 'recyclerview-v7' + } + + // Force usage of dependencies in the test app, since it is internally used by the runner module. + androidTestCompile 'com.android.support:support-annotations:' + rootProject.supportLibVersion; + androidTestCompile 'com.android.support:support-v4:' + rootProject.supportLibVersion; + androidTestCompile 'com.android.support:recyclerview-v7:' + rootProject.supportLibVersion; +} \ No newline at end of file diff --git a/PersistenceBasicSample/app/proguard-rules.pro b/PersistenceBasicSample/app/proguard-rules.pro new file mode 100644 index 000000000..4cb7103e9 --- /dev/null +++ b/PersistenceBasicSample/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/google/home/jalc/sw/android-sdks/android-sdk-linux/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/PersistenceBasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java b/PersistenceBasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java new file mode 100644 index 000000000..b9b899c39 --- /dev/null +++ b/PersistenceBasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence; + + +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.ViewModelProviders; +import android.support.annotation.Nullable; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.IdlingResource; +import android.support.test.espresso.contrib.RecyclerViewActions; +import android.support.test.rule.ActivityTestRule; +import android.support.v4.app.Fragment; + +import com.example.android.persistence.db.entity.ProductEntity; +import com.example.android.persistence.viewmodel.ProductListViewModel; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.core.IsNot.not; + +public class MainActivityTest { + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>( + MainActivity.class); + + private SimpleIdlingResource idlingRes = new SimpleIdlingResource(); + + @Before + public void idlingResourceSetup() { + + Espresso.registerIdlingResources(idlingRes); + // There's always + idlingRes.setIdleNow(false); + + ProductListViewModel productListViewModel = getProductListViewModel(); + + // Subscribe to ProductListViewModel's products list observable to figure out when the + // app is idle. + productListViewModel.getProducts().observeForever(new Observer>() { + @Override + public void onChanged(@Nullable List productEntities) { + if (productEntities != null) { + idlingRes.setIdleNow(true); + } + } + }); + } + + @Test + public void clickOnFirstItem_opensComments() { + // When clicking on the first product + onView(withContentDescription(R.string.cd_products_list)) + .perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); + + // Then the second screen with the comments should appear. + onView(withContentDescription(R.string.cd_comments_list)) + .check(matches(isDisplayed())); + + // Then the second screen with the comments should appear. + onView(withContentDescription(R.string.cd_product_name)) + .check(matches(not(withText("")))); + + } + + /** Gets the ViewModel for the current fragment */ + private ProductListViewModel getProductListViewModel() { + MainActivity activity = mActivityRule.getActivity(); + + Fragment productListFragment = activity.getSupportFragmentManager() + .findFragmentByTag(ProductListFragment.TAG); + + return ViewModelProviders.of(productListFragment) + .get(ProductListViewModel.class); + } + + private static class SimpleIdlingResource implements IdlingResource { + + // written from main thread, read from any thread. + private volatile ResourceCallback mResourceCallback; + + private AtomicBoolean mIsIdleNow = new AtomicBoolean(true); + + public void setIdleNow(boolean idleNow) { + mIsIdleNow.set(idleNow); + if (idleNow) { + mResourceCallback.onTransitionToIdle(); + } + } + + @Override + public String getName() { + return "Simple idling resource"; + } + + @Override + public boolean isIdleNow() { + return mIsIdleNow.get(); + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback callback) { + mResourceCallback = callback; + } + } +} \ No newline at end of file diff --git a/PersistenceBasicSample/app/src/main/AndroidManifest.xml b/PersistenceBasicSample/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..322d95787 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java new file mode 100644 index 000000000..aee825590 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence; + +import android.os.Bundle; +import android.support.annotation.Nullable; + +import android.arch.lifecycle.LifecycleActivity; +import com.example.android.persistence.model.Product; + +public class MainActivity extends LifecycleActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + + // Add product list fragment if this is first creation + if (savedInstanceState == null) { + ProductListFragment fragment = new ProductListFragment(); + + getSupportFragmentManager().beginTransaction() + .add(R.id.fragment_container, fragment, ProductListFragment.TAG).commit(); + } + } + + /** Shows the product detail fragment */ + public void show(Product product) { + + ProductFragment productFragment = ProductFragment.forProduct(product.getId()); + + getSupportFragmentManager() + .beginTransaction() + .addToBackStack("product") + .replace(R.id.fragment_container, + productFragment, null).commit(); + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java new file mode 100644 index 000000000..53dbcbb4e --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence; + +import android.arch.lifecycle.LifecycleFragment; +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.ViewModelProviders; +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.example.android.persistence.databinding.ProductFragmentBinding; +import com.example.android.persistence.db.entity.CommentEntity; +import com.example.android.persistence.db.entity.ProductEntity; +import com.example.android.persistence.model.Comment; +import com.example.android.persistence.ui.CommentAdapter; +import com.example.android.persistence.ui.CommentClickCallback; +import com.example.android.persistence.viewmodel.ProductViewModel; + +import java.util.List; + +public class ProductFragment extends LifecycleFragment { + + private static final String KEY_PRODUCT_ID = "product_id"; + + private ProductFragmentBinding mBinding; + + private CommentAdapter mCommentAdapter; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + // Inflate this data binding layout + mBinding = DataBindingUtil.inflate(inflater, R.layout.product_fragment, container, false); + + // Create and set the adapter for the RecyclerView. + mCommentAdapter = new CommentAdapter(mCommentClickCallback); + mBinding.commentList.setAdapter(mCommentAdapter); + return mBinding.getRoot(); + } + + private final CommentClickCallback mCommentClickCallback = new CommentClickCallback() { + @Override + public void onClick(Comment comment) { + // no-op + + } + }; + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + ProductViewModel.Factory factory = new ProductViewModel.Factory( + getActivity().getApplication(), getArguments().getInt(KEY_PRODUCT_ID)); + + final ProductViewModel model = ViewModelProviders.of(this, factory) + .get(ProductViewModel.class); + + mBinding.setProductViewModel(model); + + subscribeToModel(model); + } + + private void subscribeToModel(final ProductViewModel model) { + + model.getComments().observe(this, new Observer>() { + @Override + public void onChanged(@Nullable List commentEntities) { + if (commentEntities != null) { + mBinding.setIsLoading(false); + mCommentAdapter.setCommentList(commentEntities); + } else { + mBinding.setIsLoading(true); + } + } + }); + + model.getObservableProduct().observe(this, new Observer() { + @Override + public void onChanged(@Nullable ProductEntity productEntity) { + model.setProduct(productEntity); + } + }); + } + + /** Creates product fragment for specific product ID */ + public static ProductFragment forProduct(int productId) { + ProductFragment fragment = new ProductFragment(); + Bundle args = new Bundle(); + args.putInt(KEY_PRODUCT_ID, productId); + fragment.setArguments(args); + return fragment; + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java new file mode 100644 index 000000000..772bc9cba --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java @@ -0,0 +1,92 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence; + +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleFragment; +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.ViewModelProviders; +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.example.android.persistence.databinding.ListFragmentBinding; +import com.example.android.persistence.db.entity.ProductEntity; +import com.example.android.persistence.model.Product; +import com.example.android.persistence.ui.ProductAdapter; +import com.example.android.persistence.ui.ProductClickCallback; +import com.example.android.persistence.viewmodel.ProductListViewModel; + +import java.util.List; + +public class ProductListFragment extends LifecycleFragment { + + public static final String TAG = "ProductListViewModel"; + + private ProductAdapter mProductAdapter; + + private ListFragmentBinding mBinding; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + mBinding = DataBindingUtil.inflate(inflater, R.layout.list_fragment, container, false); + + mProductAdapter = new ProductAdapter(mProductClickCallback); + mBinding.productsList.setAdapter(mProductAdapter); + + return mBinding.getRoot(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + final ProductListViewModel viewModel = + ViewModelProviders.of(this).get(ProductListViewModel.class); + + subscribeUi(viewModel); + } + + private void subscribeUi(ProductListViewModel viewModel) { + // Update the list when the data changes + viewModel.getProducts().observe(this, new Observer>() { + @Override + public void onChanged(@Nullable List myProducts) { + if (myProducts != null) { + mBinding.setIsLoading(false); + mProductAdapter.setProductList(myProducts); + } else { + mBinding.setIsLoading(true); + } + } + }); + } + + private final ProductClickCallback mProductClickCallback = new ProductClickCallback() { + @Override + public void onClick(Product product) { + + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { + ((MainActivity) getActivity()).show(product); + } + } + }; +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java new file mode 100644 index 000000000..ce0afe4b2 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db; + +import android.arch.persistence.room.Database; +import android.arch.persistence.room.RoomDatabase; +import android.arch.persistence.room.TypeConverters; + +import com.example.android.persistence.db.dao.CommentDao; +import com.example.android.persistence.db.dao.ProductDao; +import com.example.android.persistence.db.entity.CommentEntity; +import com.example.android.persistence.db.entity.ProductEntity; +import com.example.android.persistence.db.converter.DateConverter; + +@Database(entities = {ProductEntity.class, CommentEntity.class}, version = 1) +@TypeConverters(DateConverter.class) +public abstract class AppDatabase extends RoomDatabase { + + static final String DATABASE_NAME = "basic-sample-db"; + + public abstract ProductDao productDao(); + + public abstract CommentDao commentDao(); +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java new file mode 100644 index 000000000..5ecb8847a --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java @@ -0,0 +1,123 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.persistence.room.Room; +import android.content.Context; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.example.android.persistence.db.AppDatabase.DATABASE_NAME; + +/** + * Creates the {@link AppDatabase} asynchronously, exposing a LiveData object to notify of creation. + */ +public class DatabaseCreator { + + private static DatabaseCreator sInstance; + + private final MutableLiveData mIsDatabaseCreated = new MutableLiveData<>(); + + private AppDatabase mDb; + + private final AtomicBoolean mInitializing = new AtomicBoolean(true); + + // For Singleton instantiation + private static final Object LOCK = new Object(); + + public synchronized static DatabaseCreator getInstance(Context context) { + if (sInstance == null) { + synchronized (LOCK) { + if (sInstance == null) { + sInstance = new DatabaseCreator(); + } + } + } + return sInstance; + } + + /** Used to observe when the database initialization is done */ + public LiveData isDatabaseCreated() { + return mIsDatabaseCreated; + } + + @Nullable + public AppDatabase getDatabase() { + return mDb; + } + + /** + * Creates or returns a previously-created database. + *

+ * Although this uses an AsyncTask which currently uses a serial executor, it's thread-safe. + */ + public void createDb(Context context) { + + Log.d("DatabaseCreator", "Creating DB from " + Thread.currentThread().getName()); + + if (!mInitializing.compareAndSet(true, false)) { + return; // Already initializing + } + + mIsDatabaseCreated.setValue(false);// Trigger an update to show a loading screen. + new AsyncTask() { + + @Override + protected Void doInBackground(Context... params) { + Log.d("DatabaseCreator", + "Starting bg job " + Thread.currentThread().getName()); + + Context context = params[0].getApplicationContext(); + + // Reset the database to have new data on every run. + context.deleteDatabase(DATABASE_NAME); + + // Build the database! + AppDatabase db = Room.databaseBuilder(context.getApplicationContext(), + AppDatabase.class, DATABASE_NAME).build(); + + // Add a delay to simulate a long-running operation + addDelay(); + + // Add some data to the database + DatabaseInitUtil.initializeDb(db); + Log.d("DatabaseCreator", + "DB was populated in thread " + Thread.currentThread().getName()); + + mDb = db; + return null; + } + + @Override + protected void onPostExecute(Void ignored) { + // Now on the main thread, notify observers that the db is created and ready. + mIsDatabaseCreated.setValue(true); + } + }.execute(context.getApplicationContext()); + } + + private void addDelay() { + try { + Thread.sleep(4000); + } catch (InterruptedException ignored) {} + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java new file mode 100644 index 000000000..c68fe8fad --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java @@ -0,0 +1,88 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db; + +import com.example.android.persistence.db.entity.CommentEntity; +import com.example.android.persistence.db.entity.ProductEntity; +import com.example.android.persistence.model.Product; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** Generates dummy data and inserts them into the database */ +class DatabaseInitUtil { + + private static final String[] FIRST = new String[]{ + "Special edition", "New", "Cheap", "Quality", "Used"}; + private static final String[] SECOND = new String[]{ + "Three-headed Monkey", "Rubber Chicken", "Pint of Grog", "Monocle"}; + private static final String[] DESCRIPTION = new String[]{ + "is finally here", "is recommended by Stan S. Stanman", + "is the best sold product on Mêlée Island", "is \uD83D\uDCAF", "is ❤️", "is fine"}; + private static final String[] COMMENTS = new String[]{ + "Comment 1", "Comment 2", "Comment 3", "Comment 4", "Comment 5", "Comment 6", + }; + + static void initializeDb(AppDatabase db) { + List products = new ArrayList<>(FIRST.length * SECOND.length); + List comments = new ArrayList<>(); + + generateData(products, comments); + + insertData(db, products, comments); + } + + private static void generateData(List products, List comments) { + Random rnd = new Random(); + for (int i = 0; i < FIRST.length; i++) { + for (int j = 0; j < SECOND.length; j++) { + ProductEntity product = new ProductEntity(); + product.setName(FIRST[i] + " " + SECOND[j]); + product.setDescription(product.getName() + " " + DESCRIPTION[j]); + product.setPrice(rnd.nextInt(240)); + product.setId(FIRST.length * i + j + 1); + products.add(product); + } + } + + for (Product product : products) { + int commentsNumber = rnd.nextInt(5) + 1; + for (int i = 0; i < commentsNumber; i++) { + CommentEntity comment = new CommentEntity(); + comment.setProductId(product.getId()); + comment.setText(COMMENTS[i] + " for " + product.getName()); + comment.setPostedAt(new Date(System.currentTimeMillis() + - TimeUnit.DAYS.toMillis(commentsNumber - i) + TimeUnit.HOURS.toMillis(i))); + comments.add(comment); + } + } + } + + private static void insertData(AppDatabase db, List products, List comments) { + db.beginTransaction(); + try { + db.productDao().insertAll(products); + db.commentDao().insertAll(comments); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java new file mode 100644 index 000000000..da5fe2286 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db.converter; + +import android.arch.persistence.room.TypeConverter; + +import java.util.Date; + +public class DateConverter { + @TypeConverter + public static Date toDate(Long timestamp) { + return timestamp == null ? null : new Date(timestamp); + } + + @TypeConverter + public static Long toTimestamp(Date date) { + return date == null ? null : date.getTime(); + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java new file mode 100644 index 000000000..b351526cf --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db.dao; + + +import android.arch.lifecycle.LiveData; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; +import android.arch.persistence.room.Query; + +import com.example.android.persistence.db.entity.CommentEntity; + +import java.util.List; + +@Dao +public interface CommentDao { + @Query("SELECT * FROM comments where productId = :productId") + LiveData> loadComments(int productId); + + @Query("SELECT * FROM comments where productId = :productId") + List loadCommentsSync(int productId); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAll(List products); +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java new file mode 100644 index 000000000..39407d06d --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db.dao; + +import android.arch.lifecycle.LiveData; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; +import android.arch.persistence.room.Query; + +import com.example.android.persistence.db.entity.ProductEntity; + +import java.util.List; + +@Dao +public interface ProductDao { + @Query("SELECT * FROM products") + LiveData> loadAllProducts(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAll(List products); + + @Query("select * from products where id = :productId") + LiveData loadProduct(int productId); + + @Query("select * from products where id = :productId") + ProductEntity loadProductSync(int productId); +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java new file mode 100644 index 000000000..e3d5702c1 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db.entity; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; +import android.arch.persistence.room.Index; +import android.arch.persistence.room.PrimaryKey; +import com.example.android.persistence.model.Comment; + +import java.util.Date; + +@Entity(tableName = "comments", foreignKeys = { + @ForeignKey(entity = ProductEntity.class, + parentColumns = "id", + childColumns = "productId", + onDelete = ForeignKey.CASCADE)}, indices = { + @Index(value = "productId") +}) +public class CommentEntity implements Comment { + @PrimaryKey(autoGenerate = true) + private int id; + private int productId; + private String text; + private Date postedAt; + + @Override + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public int getProductId() { + return productId; + } + + public void setProductId(int productId) { + this.productId = productId; + } + + @Override + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + @Override + public Date getPostedAt() { + return postedAt; + } + + public void setPostedAt(Date postedAt) { + this.postedAt = postedAt; + } + + public CommentEntity() { + } + + public CommentEntity(Comment comment) { + id = comment.getId(); + productId = comment.getProductId(); + text = comment.getText(); + postedAt = comment.getPostedAt(); + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java new file mode 100644 index 000000000..af1b79a9b --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.db.entity; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; + +import com.example.android.persistence.model.Product; + +@Entity(tableName = "products") +public class ProductEntity implements Product { + @PrimaryKey + private int id; + private String name; + private String description; + private int price; + + @Override + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public int getPrice() { + return price; + } + + public void setPrice(int price) { + this.price = price; + } + + public ProductEntity() { + } + + public ProductEntity(Product product) { + this.id = product.getId(); + this.name = product.getName(); + this.description = product.getDescription(); + this.price = product.getPrice(); + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java new file mode 100644 index 000000000..c3483a409 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.model; + +import java.util.Date; + +public interface Comment { + int getId(); + int getProductId(); + String getText(); + Date getPostedAt(); +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Product.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Product.java new file mode 100644 index 000000000..72e427696 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Product.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.model; + +public interface Product { + int getId(); + String getName(); + String getDescription(); + int getPrice(); +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java new file mode 100644 index 000000000..0b9033551 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.ui; + +import android.databinding.BindingAdapter; +import android.view.View; + + +public class BindingAdapters { + @BindingAdapter("visibleGone") + public static void showHide(View view, boolean show) { + view.setVisibility(show ? View.VISIBLE : View.GONE); + } +} \ No newline at end of file diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java new file mode 100644 index 000000000..24e0ecb5a --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.ui; + +import android.databinding.DataBindingUtil; +import android.support.annotation.Nullable; +import android.support.v7.util.DiffUtil; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.example.android.persistence.databinding.CommentItemBinding; +import com.example.android.persistence.model.Comment; +import com.example.android.persistence.R; + +import java.util.List; +import java.util.Objects; + +public class CommentAdapter extends RecyclerView.Adapter { + + private List mCommentList; + + @Nullable + private final CommentClickCallback mCommentClickCallback; + + public CommentAdapter(@Nullable CommentClickCallback commentClickCallback) { + mCommentClickCallback = commentClickCallback; + } + + public void setCommentList(final List comments) { + if (mCommentList == null) { + mCommentList = comments; + notifyItemRangeInserted(0, comments.size()); + } else { + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return mCommentList.size(); + } + + @Override + public int getNewListSize() { + return comments.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + Comment old = mCommentList.get(oldItemPosition); + Comment comment = comments.get(newItemPosition); + return old.getId() == comment.getId(); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + Comment old = mCommentList.get(oldItemPosition); + Comment comment = comments.get(newItemPosition); + return old.getId() == comment.getId() + && old.getPostedAt() == comment.getPostedAt() + && old.getProductId() == comment.getProductId() + && Objects.equals(old.getText(), comment.getText()); + } + }); + mCommentList = comments; + diffResult.dispatchUpdatesTo(this); + } + } + + @Override + public CommentViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + CommentItemBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), R.layout.comment_item, + parent, false); + binding.setCallback(mCommentClickCallback); + return new CommentViewHolder(binding); + } + + @Override + public void onBindViewHolder(CommentViewHolder holder, int position) { + holder.binding.setComment(mCommentList.get(position)); + holder.binding.executePendingBindings(); + } + + @Override + public int getItemCount() { + return mCommentList == null ? 0 : mCommentList.size(); + } + + static class CommentViewHolder extends RecyclerView.ViewHolder { + + final CommentItemBinding binding; + + CommentViewHolder(CommentItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java new file mode 100644 index 000000000..ced806597 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java @@ -0,0 +1,23 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.ui; + +import com.example.android.persistence.model.Comment; + +public interface CommentClickCallback { + void onClick(Comment comment); +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java new file mode 100644 index 000000000..e54c1ca2c --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java @@ -0,0 +1,110 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.ui; + +import android.databinding.DataBindingUtil; +import android.support.annotation.Nullable; +import android.support.v7.util.DiffUtil; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.example.android.persistence.databinding.ProductItemBinding; +import com.example.android.persistence.model.Product; +import com.example.android.persistence.R; + +import java.util.List; +import java.util.Objects; + +public class ProductAdapter extends RecyclerView.Adapter { + + List mProductList; + + @Nullable + private final ProductClickCallback mProductClickCallback; + + public ProductAdapter(@Nullable ProductClickCallback clickCallback) { + mProductClickCallback = clickCallback; + } + + public void setProductList(final List productList) { + if (mProductList == null) { + mProductList = productList; + notifyItemRangeInserted(0, productList.size()); + } else { + DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return mProductList.size(); + } + + @Override + public int getNewListSize() { + return productList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mProductList.get(oldItemPosition).getId() == + productList.get(newItemPosition).getId(); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + Product product = productList.get(newItemPosition); + Product old = productList.get(oldItemPosition); + return product.getId() == old.getId() + && Objects.equals(product.getDescription(), old.getDescription()) + && Objects.equals(product.getName(), old.getName()) + && product.getPrice() == old.getPrice(); + } + }); + mProductList = productList; + result.dispatchUpdatesTo(this); + } + } + + @Override + public ProductViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + ProductItemBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), R.layout.product_item, + parent, false); + binding.setCallback(mProductClickCallback); + return new ProductViewHolder(binding); + } + + @Override + public void onBindViewHolder(ProductViewHolder holder, int position) { + holder.binding.setProduct(mProductList.get(position)); + holder.binding.executePendingBindings(); + } + + @Override + public int getItemCount() { + return mProductList == null ? 0 : mProductList.size(); + } + + static class ProductViewHolder extends RecyclerView.ViewHolder { + + final ProductItemBinding binding; + + public ProductViewHolder(ProductItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java new file mode 100644 index 000000000..b8f4c6b78 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java @@ -0,0 +1,23 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.ui; + +import com.example.android.persistence.model.Product; + +public interface ProductClickCallback { + void onClick(Product product); +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java new file mode 100644 index 000000000..8905b14f9 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.viewmodel; + +import android.app.Application; +import android.arch.core.util.Function; +import android.arch.lifecycle.AndroidViewModel; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Transformations; + +import com.example.android.persistence.db.DatabaseCreator; +import com.example.android.persistence.db.entity.ProductEntity; + +import java.util.List; + +public class ProductListViewModel extends AndroidViewModel { + + private static final MutableLiveData ABSENT = new MutableLiveData(); + { + //noinspection unchecked + ABSENT.setValue(null); + } + + private final LiveData> mObservableProducts; + + public ProductListViewModel(Application application) { + super(application); + + final DatabaseCreator databaseCreator = DatabaseCreator.getInstance(this.getApplication()); + + LiveData databaseCreated = databaseCreator.isDatabaseCreated(); + mObservableProducts = Transformations.switchMap(databaseCreated, + new Function>>() { + @Override + public LiveData> apply(Boolean isDbCreated) { + if (!Boolean.TRUE.equals(isDbCreated)) { // Not needed here, but watch out for null + //noinspection unchecked + return ABSENT; + } else { + //noinspection ConstantConditions + return databaseCreator.getDatabase().productDao().loadAllProducts(); + } + } + }); + + databaseCreator.createDb(this.getApplication()); + } + + /** + * Expose the LiveData Products query so the UI can observe it. + */ + public LiveData> getProducts() { + return mObservableProducts; + } +} diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java new file mode 100644 index 000000000..be524d516 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java @@ -0,0 +1,134 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.persistence.viewmodel; + +import android.app.Application; +import android.arch.core.util.Function; +import android.arch.lifecycle.AndroidViewModel; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.Transformations; +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProvider; +import android.databinding.ObservableField; +import android.support.annotation.NonNull; + +import com.example.android.persistence.db.DatabaseCreator; +import com.example.android.persistence.db.entity.CommentEntity; +import com.example.android.persistence.db.entity.ProductEntity; + +import java.util.List; + +public class ProductViewModel extends AndroidViewModel { + + private static final MutableLiveData ABSENT = new MutableLiveData(); + + private final LiveData mObservableProduct; + + private Observer mProductObserver; + + public ObservableField product = new ObservableField<>(); + + { + //noinspection unchecked + ABSENT.setValue(null); + } + + // Product exposed for data binding + //public final ObservableField product = new ObservableField<>(); + + private final int mProductId; + + private final LiveData> mObservableComments; + + public ProductViewModel(@NonNull Application application, + final int productId) { + super(application); + mProductId = productId; + + final DatabaseCreator databaseCreator = DatabaseCreator.getInstance(this.getApplication()); + + mObservableComments = Transformations.switchMap(databaseCreator.isDatabaseCreated(), new Function>>() { + @Override + public LiveData> apply(Boolean isDbCreated) { + if (!isDbCreated) { + //noinspection unchecked + return ABSENT; + } else { + //noinspection ConstantConditions + return databaseCreator.getDatabase().commentDao().loadComments(mProductId); + } + } + }); + + mObservableProduct = Transformations.switchMap(databaseCreator.isDatabaseCreated(), new Function>() { + @Override + public LiveData apply(Boolean isDbCreated) { + if (!isDbCreated) { + //noinspection unchecked + return ABSENT; + } else { + //noinspection ConstantConditions + return databaseCreator.getDatabase().productDao().loadProduct(mProductId); + } + } + }); + + databaseCreator.createDb(this.getApplication()); + + } + /** + * Expose the LiveData Comments query so the UI can observe it. + */ + public LiveData> getComments() { + return mObservableComments; + } + + public LiveData getObservableProduct() { + return mObservableProduct; + } + + public void setProduct(ProductEntity product) { + this.product.set(product); + } + + /** + * A creator is used to inject the product ID into the ViewModel + *

+ * This creator is to showcase how to inject dependencies into ViewModels. It's not + * actually necessary in this case, as the product ID can be passed in a public method. + */ + public static class Factory extends ViewModelProvider.NewInstanceFactory { + + @NonNull + private final Application mApplication; + + private final int mProductId; + + public Factory(@NonNull Application application, int productId) { + mApplication = application; + mProductId = productId; + } + + @Override + public T create(Class modelClass) { + //noinspection unchecked + return (T) new ProductViewModel(mApplication, mProductId); + } + } +} diff --git a/PersistenceBasicSample/app/src/main/res/layout/comment_item.xml b/PersistenceBasicSample/app/src/main/res/layout/comment_item.xml new file mode 100644 index 000000000..e1bf7f4bb --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/layout/comment_item.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/PersistenceBasicSample/app/src/main/res/layout/list_fragment.xml b/PersistenceBasicSample/app/src/main/res/layout/list_fragment.xml new file mode 100644 index 000000000..4f1d382e6 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/layout/list_fragment.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PersistenceBasicSample/app/src/main/res/layout/main_activity.xml b/PersistenceBasicSample/app/src/main/res/layout/main_activity.xml new file mode 100644 index 000000000..29ee166fa --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/layout/main_activity.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/PersistenceBasicSample/app/src/main/res/layout/product_fragment.xml b/PersistenceBasicSample/app/src/main/res/layout/product_fragment.xml new file mode 100644 index 000000000..f80d69f7f --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/layout/product_fragment.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PersistenceBasicSample/app/src/main/res/layout/product_item.xml b/PersistenceBasicSample/app/src/main/res/layout/product_item.xml new file mode 100644 index 000000000..aa85489de --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/layout/product_item.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png b/PersistenceBasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e19d44f531f36af8c908fd79144c4b56c9bd68a4 GIT binary patch literal 3118 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 P00000NkvXXu0mjf^(9Pp literal 0 HcmV?d00001 diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/PersistenceBasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9ae37253d68bb2a4b89a1f06b749399e4fd74225 GIT binary patch literal 3618 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 literal 0 HcmV?d00001 diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/PersistenceBasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6c3aadde8c3445bd2b6e5e7377c035de1174e7 GIT binary patch literal 6209 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 O6QHW7sZjC+74&~##-h6b literal 0 HcmV?d00001 diff --git a/PersistenceBasicSample/app/src/main/res/values/colors.xml b/PersistenceBasicSample/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..c481ea436 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/values/colors.xml @@ -0,0 +1,24 @@ + + + + + #3F51B5 + #303F9F + #FF4081 + + #d6d6d6 + diff --git a/PersistenceBasicSample/app/src/main/res/values/dimens.xml b/PersistenceBasicSample/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..265e36772 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/values/dimens.xml @@ -0,0 +1,25 @@ + + + + + 16dp + 8dp + 16dp + 16dp + 8dp + 64dp + diff --git a/PersistenceBasicSample/app/src/main/res/values/product_app.xml b/PersistenceBasicSample/app/src/main/res/values/product_app.xml new file mode 100644 index 000000000..d1fe8191b --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/values/product_app.xml @@ -0,0 +1,21 @@ + + + + + Price: $%d + 100dp + \ No newline at end of file diff --git a/PersistenceBasicSample/app/src/main/res/values/strings.xml b/PersistenceBasicSample/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..04a5057a6 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/values/strings.xml @@ -0,0 +1,25 @@ + + + + Persistence sample + No comments + Loading comments... + Loading products... + Products list + Comments list + Name of the product + diff --git a/PersistenceBasicSample/app/src/main/res/values/styles.xml b/PersistenceBasicSample/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..7aa722884 --- /dev/null +++ b/PersistenceBasicSample/app/src/main/res/values/styles.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/PersistenceBasicSample/build.gradle b/PersistenceBasicSample/build.gradle new file mode 100644 index 000000000..7da1fc6e1 --- /dev/null +++ b/PersistenceBasicSample/build.gradle @@ -0,0 +1,52 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + maven { + url "https://android-devtools-staging.corp.google.com/no_crawl/maven2" + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + buildToolsVersion = "25.0.2" + supportLibVersion = "25.3.1" + runnerVersion = "0.5" + rulesVersion = "0.5" + espressoVersion = "2.2.2" + archLifecycleVersion = "0.9.0" + archRoomVersion = "0.9.0" +} \ No newline at end of file diff --git a/PersistenceBasicSample/gradle.properties b/PersistenceBasicSample/gradle.properties new file mode 100644 index 000000000..684bee600 --- /dev/null +++ b/PersistenceBasicSample/gradle.properties @@ -0,0 +1,33 @@ +# +# Copyright 2017, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/PersistenceBasicSample/gradle/wrapper/gradle-wrapper.jar b/PersistenceBasicSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/PersistenceBasicSample/gradle/wrapper/gradle-wrapper.properties b/PersistenceBasicSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..37b4e8c5f --- /dev/null +++ b/PersistenceBasicSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,22 @@ +# +# Copyright 2017, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#Mon Apr 24 18:19:01 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/PersistenceBasicSample/gradlew b/PersistenceBasicSample/gradlew new file mode 100755 index 000000000..9d82f7891 --- /dev/null +++ b/PersistenceBasicSample/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/PersistenceBasicSample/gradlew.bat b/PersistenceBasicSample/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/PersistenceBasicSample/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/PersistenceBasicSample/settings.gradle b/PersistenceBasicSample/settings.gradle new file mode 100644 index 000000000..a266f7d18 --- /dev/null +++ b/PersistenceBasicSample/settings.gradle @@ -0,0 +1,17 @@ +/* + * Copyright 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +include ':app' diff --git a/PersistenceContentProviderSample/.gitignore b/PersistenceContentProviderSample/.gitignore new file mode 100644 index 000000000..39fb081a4 --- /dev/null +++ b/PersistenceContentProviderSample/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/PersistenceContentProviderSample/app/.gitignore b/PersistenceContentProviderSample/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/PersistenceContentProviderSample/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/PersistenceContentProviderSample/app/build.gradle b/PersistenceContentProviderSample/app/build.gradle new file mode 100644 index 000000000..fc9ac381c --- /dev/null +++ b/PersistenceContentProviderSample/app/build.gradle @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "com.example.android.contentprovidersample" + minSdkVersion 14 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + // Write out the current schema of Room + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + + // Test + testCompile 'junit:junit:4.12' + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + + // Support Libraries + compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:recyclerview-v7:25.3.1' + + // App Toolkit + compile "android.arch.lifecycle:extensions:0.9.0" + compile "android.arch.persistence.room:runtime:0.9.0" + annotationProcessor "android.arch.lifecycle:compiler:0.9.0" + annotationProcessor "android.arch.persistence.room:compiler:0.9.0" +} diff --git a/PersistenceContentProviderSample/app/proguard-rules.pro b/PersistenceContentProviderSample/app/proguard-rules.pro new file mode 100644 index 000000000..06b266fc9 --- /dev/null +++ b/PersistenceContentProviderSample/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/google/home/yaraki/Android/Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/PersistenceContentProviderSample/app/schemas/com.example.android.contentprovidersample.data.SampleDatabase/1.json b/PersistenceContentProviderSample/app/schemas/com.example.android.contentprovidersample.data.SampleDatabase/1.json new file mode 100644 index 000000000..d154d0713 --- /dev/null +++ b/PersistenceContentProviderSample/app/schemas/com.example.android.contentprovidersample.data.SampleDatabase/1.json @@ -0,0 +1,46 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "d612c3c0739239af787986e19d97626b", + "entities": [ + { + "tableName": "cheeses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_cheeses__id", + "unique": false, + "columnNames": [ + "_id" + ], + "createSql": "CREATE INDEX `index_cheeses__id` ON `${TABLE_NAME}` (`_id`)" + } + ], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d612c3c0739239af787986e19d97626b\")" + ] + } +} \ No newline at end of file diff --git a/PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/CheeseTest.java b/PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/CheeseTest.java new file mode 100644 index 000000000..7ae4ee60f --- /dev/null +++ b/PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/CheeseTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.contentprovidersample; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import android.arch.persistence.room.Room; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.example.android.contentprovidersample.data.Cheese; +import com.example.android.contentprovidersample.data.SampleDatabase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CheeseTest { + + private SampleDatabase mDatabase; + + @Before + public void createDatabase() { + mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), + SampleDatabase.class).build(); + } + + @After + public void closeDatabase() throws IOException { + mDatabase.close(); + } + + @Test + public void insertAndCount() { + assertThat(mDatabase.cheese().count(), is(0)); + Cheese cheese = new Cheese(); + cheese.name = "abc"; + mDatabase.cheese().insert(cheese); + assertThat(mDatabase.cheese().count(), is(1)); + } + +} diff --git a/PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/SampleContentProviderTest.java b/PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/SampleContentProviderTest.java new file mode 100644 index 000000000..860a55150 --- /dev/null +++ b/PersistenceContentProviderSample/app/src/androidTest/java/com/example/android/contentprovidersample/SampleContentProviderTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.contentprovidersample; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.example.android.contentprovidersample.data.Cheese; +import com.example.android.contentprovidersample.data.SampleDatabase; +import com.example.android.contentprovidersample.provider.SampleContentProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SampleContentProviderTest { + + private ContentResolver mContentResolver; + + @Before + public void setUp() { + final Context context = InstrumentationRegistry.getTargetContext(); + SampleDatabase.switchToInMemory(context); + mContentResolver = context.getContentResolver(); + } + + @Test + public void cheese_initiallyEmpty() { + final Cursor cursor = mContentResolver.query(SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, null, null, null); + assertThat(cursor, notNullValue()); + assertThat(cursor.getCount(), is(0)); + cursor.close(); + } + + @Test + public void cheese_insert() { + final Uri itemUri = mContentResolver.insert(SampleContentProvider.URI_CHEESE, + cheeseWithName("Daigo")); + assertThat(itemUri, notNullValue()); + final Cursor cursor = mContentResolver.query(SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, null, null, null); + assertThat(cursor, notNullValue()); + assertThat(cursor.getCount(), is(1)); + assertThat(cursor.moveToFirst(), is(true)); + assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Cheese.COLUMN_NAME)), is("Daigo")); + cursor.close(); + } + + @Test + public void cheese_update() { + final Uri itemUri = mContentResolver.insert(SampleContentProvider.URI_CHEESE, + cheeseWithName("Daigo")); + assertThat(itemUri, notNullValue()); + final int count = mContentResolver.update(itemUri, cheeseWithName("Queso"), null, null); + assertThat(count, is(1)); + final Cursor cursor = mContentResolver.query(SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, null, null, null); + assertThat(cursor, notNullValue()); + assertThat(cursor.getCount(), is(1)); + assertThat(cursor.moveToFirst(), is(true)); + assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Cheese.COLUMN_NAME)), is("Queso")); + cursor.close(); + } + + @Test + public void cheese_delete() { + final Uri itemUri = mContentResolver.insert(SampleContentProvider.URI_CHEESE, + cheeseWithName("Daigo")); + assertThat(itemUri, notNullValue()); + final Cursor cursor1 = mContentResolver.query(SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, null, null, null); + assertThat(cursor1, notNullValue()); + assertThat(cursor1.getCount(), is(1)); + cursor1.close(); + final int count = mContentResolver.delete(itemUri, null, null); + assertThat(count, is(1)); + final Cursor cursor2 = mContentResolver.query(SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, null, null, null); + assertThat(cursor2, notNullValue()); + assertThat(cursor2.getCount(), is(0)); + cursor2.close(); + } + + @Test + public void cheese_bulkInsert() { + final int count = mContentResolver.bulkInsert(SampleContentProvider.URI_CHEESE, + new ContentValues[]{ + cheeseWithName("Peynir"), + cheeseWithName("Queso"), + cheeseWithName("Daigo"), + }); + assertThat(count, is(3)); + final Cursor cursor = mContentResolver.query(SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, null, null, null); + assertThat(cursor, notNullValue()); + assertThat(cursor.getCount(), is(3)); + cursor.close(); + } + + @Test + public void cheese_applyBatch() throws RemoteException, OperationApplicationException { + final ArrayList operations = new ArrayList<>(); + operations.add(ContentProviderOperation + .newInsert(SampleContentProvider.URI_CHEESE) + .withValue(Cheese.COLUMN_NAME, "Peynir") + .build()); + operations.add(ContentProviderOperation + .newInsert(SampleContentProvider.URI_CHEESE) + .withValue(Cheese.COLUMN_NAME, "Queso") + .build()); + final ContentProviderResult[] results = mContentResolver.applyBatch( + SampleContentProvider.AUTHORITY, operations); + assertThat(results.length, is(2)); + final Cursor cursor = mContentResolver.query(SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, null, null, null); + assertThat(cursor, notNullValue()); + assertThat(cursor.getCount(), is(2)); + assertThat(cursor.moveToFirst(), is(true)); + cursor.close(); + } + + private ContentValues cheeseWithName(String name) { + final ContentValues values = new ContentValues(); + values.put(Cheese.COLUMN_NAME, name); + return values; + } + +} diff --git a/PersistenceContentProviderSample/app/src/main/AndroidManifest.xml b/PersistenceContentProviderSample/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..82d25e4b2 --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/MainActivity.java b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/MainActivity.java new file mode 100644 index 000000000..89db65f8b --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/MainActivity.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.contentprovidersample; + +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.example.android.contentprovidersample.data.Cheese; +import com.example.android.contentprovidersample.provider.SampleContentProvider; + + +/** + * Not very relevant to Room. This just shows data from {@link SampleContentProvider}. + * + *

Since the data is exposed through the ContentProvider, other apps can read and write the + * content in a similar manner to this.

+ */ +public class MainActivity extends AppCompatActivity { + + private static final int LOADER_CHEESES = 1; + + private CheeseAdapter mCheeseAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + + final RecyclerView list = (RecyclerView) findViewById(R.id.list); + list.setLayoutManager(new LinearLayoutManager(list.getContext())); + mCheeseAdapter = new CheeseAdapter(); + list.setAdapter(mCheeseAdapter); + + getSupportLoaderManager().initLoader(LOADER_CHEESES, null, mLoaderCallbacks); + } + + private LoaderManager.LoaderCallbacks mLoaderCallbacks = + new LoaderManager.LoaderCallbacks() { + + @Override + public Loader onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_CHEESES: + return new CursorLoader(getApplicationContext(), + SampleContentProvider.URI_CHEESE, + new String[]{Cheese.COLUMN_NAME}, + null, null, null); + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + switch (loader.getId()) { + case LOADER_CHEESES: + mCheeseAdapter.setCheeses(data); + break; + } + } + + @Override + public void onLoaderReset(Loader loader) { + switch (loader.getId()) { + case LOADER_CHEESES: + mCheeseAdapter.setCheeses(null); + break; + } + } + + }; + + private static class CheeseAdapter extends RecyclerView.Adapter { + + private Cursor mCursor; + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(parent); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + if (mCursor.moveToPosition(position)) { + holder.mText.setText(mCursor.getString( + mCursor.getColumnIndexOrThrow(Cheese.COLUMN_NAME))); + } + } + + @Override + public int getItemCount() { + return mCursor == null ? 0 : mCursor.getCount(); + } + + void setCheeses(Cursor cursor) { + mCursor = cursor; + notifyDataSetChanged(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + + TextView mText; + + ViewHolder(ViewGroup parent) { + super(LayoutInflater.from(parent.getContext()).inflate( + android.R.layout.simple_list_item_1, parent, false)); + mText = (TextView) itemView.findViewById(android.R.id.text1); + } + + } + + } + +} diff --git a/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/Cheese.java b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/Cheese.java new file mode 100644 index 000000000..4534e13c5 --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/Cheese.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.contentprovidersample.data; + +import android.arch.persistence.room.ColumnInfo; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.PrimaryKey; +import android.content.ContentValues; +import android.provider.BaseColumns; + + +/** + * Represents one record of the Cheese table. + */ +@Entity(tableName = Cheese.TABLE_NAME) +public class Cheese { + + /** The name of the Cheese table. */ + public static final String TABLE_NAME = "cheeses"; + + /** The name of the ID column. */ + public static final String COLUMN_ID = BaseColumns._ID; + + /** The name of the name column. */ + public static final String COLUMN_NAME = "name"; + + /** The unique ID of the cheese. */ + @PrimaryKey(autoGenerate = true) + @ColumnInfo(index = true, name = COLUMN_ID) + public long id; + + /** The name of the cheese. */ + @ColumnInfo(name = COLUMN_NAME) + public String name; + + /** + * Create a new {@link Cheese} from the specified {@link ContentValues}. + * + * @param values A {@link ContentValues} that at least contain {@link #COLUMN_NAME}. + * @return A newly created {@link Cheese} instance. + */ + public static Cheese fromContentValues(ContentValues values) { + final Cheese cheese = new Cheese(); + if (values.containsKey(COLUMN_ID)) { + cheese.id = values.getAsLong(COLUMN_ID); + } + if (values.containsKey(COLUMN_NAME)) { + cheese.name = values.getAsString(COLUMN_NAME); + } + return cheese; + } + + /** Dummy data. */ + static final String[] CHEESES = { + "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi", + "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", + "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese", + "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell", + "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc", + "Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss", + "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon", + "Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase", + "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese", + "Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy", + "Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille", + "Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore", + "Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)", + "Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves", + "Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur", + "Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon", + "Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin", + "Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)", + "Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine", + "Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza", + "Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)", + "Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta", + "Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie", + "Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat", + "Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano", + "Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain", + "Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou", + "Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar", + "Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno", + "Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack", + "Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper", + "Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)", + "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese", + "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza", + "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley", + "Cuajada", "Curd", "Cure Nantais", "Curworthy", "Cwmtawe Pecorino", + "Cypress Grove Chevre", "Danablu (Danish Blue)", "Danbo", "Danish Fontina", + "Daralagjazsky", "Dauphin", "Delice des Fiouves", "Denhany Dorset Drum", "Derby", + "Dessertnyj Belyj", "Devon Blue", "Devon Garland", "Dolcelatte", "Doolin", + "Doppelrhamstufel", "Dorset Blue Vinney", "Double Gloucester", "Double Worcester", + "Dreux a la Feuille", "Dry Jack", "Duddleswell", "Dunbarra", "Dunlop", "Dunsyre Blue", + "Duroblando", "Durrus", "Dutch Mimolette (Commissiekaas)", "Edam", "Edelpilz", + "Emental Grand Cru", "Emlett", "Emmental", "Epoisses de Bourgogne", "Esbareich", + "Esrom", "Etorki", "Evansdale Farmhouse Brie", "Evora De L'Alentejo", "Exmoor Blue", + "Explorateur", "Feta", "Feta (Australian)", "Figue", "Filetta", "Fin-de-Siecle", + "Finlandia Swiss", "Finn", "Fiore Sardo", "Fleur du Maquis", "Flor de Guia", + "Flower Marie", "Folded", "Folded cheese with mint", "Fondant de Brebis", + "Fontainebleau", "Fontal", "Fontina Val d'Aosta", "Formaggio di capra", "Fougerus", + "Four Herb Gouda", "Fourme d' Ambert", "Fourme de Haute Loire", "Fourme de Montbrison", + "Fresh Jack", "Fresh Mozzarella", "Fresh Ricotta", "Fresh Truffles", "Fribourgeois", + "Friesekaas", "Friesian", "Friesla", "Frinault", "Fromage a Raclette", "Fromage Corse", + "Fromage de Montagne de Savoie", "Fromage Frais", "Fruit Cream Cheese", + "Frying Cheese", "Fynbo", "Gabriel", "Galette du Paludier", "Galette Lyonnaise", + "Galloway Goat's Milk Gems", "Gammelost", "Gaperon a l'Ail", "Garrotxa", "Gastanberra", + "Geitost", "Gippsland Blue", "Gjetost", "Gloucester", "Golden Cross", "Gorgonzola", + "Gornyaltajski", "Gospel Green", "Gouda", "Goutu", "Gowrie", "Grabetto", "Graddost", + "Grafton Village Cheddar", "Grana", "Grana Padano", "Grand Vatel", + "Grataron d' Areches", "Gratte-Paille", "Graviera", "Greuilh", "Greve", + "Gris de Lille", "Gruyere", "Gubbeen", "Guerbigny", "Halloumi", + "Halloumy (Australian)", "Haloumi-Style Cheese", "Harbourne Blue", "Havarti", + "Heidi Gruyere", "Hereford Hop", "Herrgardsost", "Herriot Farmhouse", "Herve", + "Hipi Iti", "Hubbardston Blue Cow", "Hushallsost", "Iberico", "Idaho Goatster", + "Idiazabal", "Il Boschetto al Tartufo", "Ile d'Yeu", "Isle of Mull", "Jarlsberg", + "Jermi Tortes", "Jibneh Arabieh", "Jindi Brie", "Jubilee Blue", "Juustoleipa", + "Kadchgall", "Kaseri", "Kashta", "Kefalotyri", "Kenafa", "Kernhem", "Kervella Affine", + "Kikorangi", "King Island Cape Wickham Brie", "King River Gold", "Klosterkaese", + "Knockalara", "Kugelkase", "L'Aveyronnais", "L'Ecir de l'Aubrac", "La Taupiniere", + "La Vache Qui Rit", "Laguiole", "Lairobell", "Lajta", "Lanark Blue", "Lancashire", + "Langres", "Lappi", "Laruns", "Lavistown", "Le Brin", "Le Fium Orbo", "Le Lacandou", + "Le Roule", "Leafield", "Lebbene", "Leerdammer", "Leicester", "Leyden", "Limburger", + "Lincolnshire Poacher", "Lingot Saint Bousquet d'Orb", "Liptauer", "Little Rydings", + "Livarot", "Llanboidy", "Llanglofan Farmhouse", "Loch Arthur Farmhouse", + "Loddiswell Avondale", "Longhorn", "Lou Palou", "Lou Pevre", "Lyonnais", "Maasdam", + "Macconais", "Mahoe Aged Gouda", "Mahon", "Malvern", "Mamirolle", "Manchego", + "Manouri", "Manur", "Marble Cheddar", "Marbled Cheeses", "Maredsous", "Margotin", + "Maribo", "Maroilles", "Mascares", "Mascarpone", "Mascarpone (Australian)", + "Mascarpone Torta", "Matocq", "Maytag Blue", "Meira", "Menallack Farmhouse", + "Menonita", "Meredith Blue", "Mesost", "Metton (Cancoillotte)", "Meyer Vintage Gouda", + "Mihalic Peynir", "Milleens", "Mimolette", "Mine-Gabhar", "Mini Baby Bells", "Mixte", + "Molbo", "Monastery Cheeses", "Mondseer", "Mont D'or Lyonnais", "Montasio", + "Monterey Jack", "Monterey Jack Dry", "Morbier", "Morbier Cru de Montagne", + "Mothais a la Feuille", "Mozzarella", "Mozzarella (Australian)", + "Mozzarella di Bufala", "Mozzarella Fresh, in water", "Mozzarella Rolls", "Munster", + "Murol", "Mycella", "Myzithra", "Naboulsi", "Nantais", "Neufchatel", + "Neufchatel (Australian)", "Niolo", "Nokkelost", "Northumberland", "Oaxaca", + "Olde York", "Olivet au Foin", "Olivet Bleu", "Olivet Cendre", + "Orkney Extra Mature Cheddar", "Orla", "Oschtjepka", "Ossau Fermier", "Ossau-Iraty", + "Oszczypek", "Oxford Blue", "P'tit Berrichon", "Palet de Babligny", "Paneer", "Panela", + "Pannerone", "Pant ys Gawn", "Parmesan (Parmigiano)", "Parmigiano Reggiano", + "Pas de l'Escalette", "Passendale", "Pasteurized Processed", "Pate de Fromage", + "Patefine Fort", "Pave d'Affinois", "Pave d'Auge", "Pave de Chirac", "Pave du Berry", + "Pecorino", "Pecorino in Walnut Leaves", "Pecorino Romano", "Peekskill Pyramid", + "Pelardon des Cevennes", "Pelardon des Corbieres", "Penamellera", "Penbryn", + "Pencarreg", "Perail de Brebis", "Petit Morin", "Petit Pardou", "Petit-Suisse", + "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin", + "Plateau de Herve", "Plymouth Cheese", "Podhalanski", "Poivre d'Ane", "Polkolbin", + "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre", + "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone", + "Provolone (Australian)", "Pyengana Cheddar", "Pyramide", "Quark", + "Quark (Australian)", "Quartirolo Lombardo", "Quatre-Vents", "Quercy Petit", + "Queso Blanco", "Queso Blanco con Frutas --Pina y Mango", "Queso de Murcia", + "Queso del Montsec", "Queso del Tietar", "Queso Fresco", "Queso Fresco (Adobera)", + "Queso Iberico", "Queso Jalapeno", "Queso Majorero", "Queso Media Luna", + "Queso Para Frier", "Queso Quesadilla", "Rabacal", "Raclette", "Ragusano", "Raschera", + "Reblochon", "Red Leicester", "Regal de la Dombes", "Reggianito", "Remedou", + "Requeson", "Richelieu", "Ricotta", "Ricotta (Australian)", "Ricotta Salata", "Ridder", + "Rigotte", "Rocamadour", "Rollot", "Romano", "Romans Part Dieu", "Roncal", "Roquefort", + "Roule", "Rouleau De Beaulieu", "Royalp Tilsit", "Rubens", "Rustinu", "Saaland Pfarr", + "Saanenkaese", "Saga", "Sage Derby", "Sainte Maure", "Saint-Marcellin", + "Saint-Nectaire", "Saint-Paulin", "Salers", "Samso", "San Simon", "Sancerre", + "Sap Sago", "Sardo", "Sardo Egyptian", "Sbrinz", "Scamorza", "Schabzieger", "Schloss", + "Selles sur Cher", "Selva", "Serat", "Seriously Strong Cheddar", "Serra da Estrela", + "Sharpam", "Shelburne Cheddar", "Shropshire Blue", "Siraz", "Sirene", "Smoked Gouda", + "Somerset Brie", "Sonoma Jack", "Sottocenare al Tartufo", "Soumaintrain", + "Sourire Lozerien", "Spenwood", "Sraffordshire Organic", "St. Agur Blue Cheese", + "Stilton", "Stinking Bishop", "String", "Sussex Slipcote", "Sveciaost", "Swaledale", + "Sweet Style Swiss", "Swiss", "Syrian (Armenian String)", "Tala", "Taleggio", "Tamie", + "Tasmania Highland Chevre Log", "Taupiniere", "Teifi", "Telemea", "Testouri", + "Tete de Moine", "Tetilla", "Texas Goat Cheese", "Tibet", "Tillamook Cheddar", + "Tilsit", "Timboon Brie", "Toma", "Tomme Brulee", "Tomme d'Abondance", + "Tomme de Chevre", "Tomme de Romans", "Tomme de Savoie", "Tomme des Chouans", "Tommes", + "Torta del Casar", "Toscanello", "Touree de L'Aubier", "Tourmalet", + "Trappe (Veritable)", "Trois Cornes De Vendee", "Tronchon", "Trou du Cru", "Truffe", + "Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa", + "Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois", + "Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue", + "Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington", + "Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou", + "Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue", + "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano" + }; + +} diff --git a/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/CheeseDao.java b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/CheeseDao.java new file mode 100644 index 000000000..ea007c50b --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/CheeseDao.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.contentprovidersample.data; + +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.Update; +import android.database.Cursor; + + +/** + * Data access object for Cheese. + */ +@Dao +public interface CheeseDao { + + /** + * Counts the number of cheeses in the table. + * + * @return The number of cheeses. + */ + @Query("SELECT COUNT(*) FROM " + Cheese.TABLE_NAME) + int count(); + + /** + * Inserts a cheese into the table. + * + * @param cheese A new cheese. + * @return The row ID of the newly inserted cheese. + */ + @Insert + long insert(Cheese cheese); + + /** + * Inserts multiple cheeses into the database + * + * @param cheeses An array of new cheeses. + * @return The row IDs of the newly inserted cheeses. + */ + @Insert + long[] insertAll(Cheese[] cheeses); + + /** + * Select all cheeses. + * + * @return A {@link Cursor} of all the cheeses in the table. + */ + @Query("SELECT * FROM " + Cheese.TABLE_NAME) + Cursor selectAll(); + + /** + * Select a cheese by the ID. + * + * @param id The row ID. + * @return A {@link Cursor} of the selected cheese. + */ + @Query("SELECT * FROM " + Cheese.TABLE_NAME + " WHERE " + Cheese.COLUMN_ID + " = :id") + Cursor selectById(long id); + + /** + * Delete a cheese by the ID. + * + * @param id The row ID. + * @return A number of cheeses deleted. This should always be {@code 1}. + */ + @Query("DELETE FROM " + Cheese.TABLE_NAME + " WHERE " + Cheese.COLUMN_ID + " = :id") + int deleteById(long id); + + /** + * Update the cheese. The cheese is identified by the row ID. + * + * @param cheese The cheese to update. + * @return A number of cheeses updated. This should always be {@code 1}. + */ + @Update + int update(Cheese cheese); + +} diff --git a/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/SampleDatabase.java b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/SampleDatabase.java new file mode 100644 index 000000000..9f27bf005 --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/data/SampleDatabase.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.contentprovidersample.data; + +import android.arch.persistence.room.Database; +import android.arch.persistence.room.Room; +import android.arch.persistence.room.RoomDatabase; +import android.content.Context; +import android.support.annotation.VisibleForTesting; + + +/** + * The Room database. + */ +@Database(entities = {Cheese.class}, version = 1) +public abstract class SampleDatabase extends RoomDatabase { + + /** + * @return The DAO for the Cheese table. + */ + @SuppressWarnings("WeakerAccess") + public abstract CheeseDao cheese(); + + /** The only instance */ + private static SampleDatabase sInstance; + + /** + * Gets the singleton instance of SampleDatabase. + * + * @param context The context. + * @return The singleton instance of SampleDatabase. + */ + public static synchronized SampleDatabase getInstance(Context context) { + if (sInstance == null) { + sInstance = Room + .databaseBuilder(context.getApplicationContext(), SampleDatabase.class, "ex") + .build(); + sInstance.populateInitialData(); + } + return sInstance; + } + + /** + * Switches the internal implementation with an empty in-memory database. + * + * @param context The context. + */ + @VisibleForTesting + public static void switchToInMemory(Context context) { + sInstance = Room.inMemoryDatabaseBuilder(context.getApplicationContext(), + SampleDatabase.class).build(); + } + + /** + * Inserts the dummy data into the database if it is currently empty. + */ + private void populateInitialData() { + if (cheese().count() == 0) { + Cheese cheese = new Cheese(); + beginTransaction(); + try { + for (int i = 0; i < Cheese.CHEESES.length; i++) { + cheese.name = Cheese.CHEESES[i]; + cheese().insert(cheese); + } + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + } + +} diff --git a/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/provider/SampleContentProvider.java b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/provider/SampleContentProvider.java new file mode 100644 index 000000000..00ae751c5 --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/java/com/example/android/contentprovidersample/provider/SampleContentProvider.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.contentprovidersample.provider; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.example.android.contentprovidersample.data.Cheese; +import com.example.android.contentprovidersample.data.CheeseDao; +import com.example.android.contentprovidersample.data.SampleDatabase; + +import java.util.ArrayList; + + +/** + * A {@link ContentProvider} based on a Room database. + * + *

Note that you don't need to implement a ContentProvider unless you want to expose the data + * outside your process or your application already uses a ContentProvider.

+ */ +public class SampleContentProvider extends ContentProvider { + + /** The authority of this content provider. */ + public static final String AUTHORITY = "com.example.android.contentprovidersample.provider"; + + /** The URI for the Cheese table. */ + public static final Uri URI_CHEESE = Uri.parse( + "content://" + AUTHORITY + "/" + Cheese.TABLE_NAME); + + /** The match code for some items in the Cheese table. */ + private static final int CODE_CHEESE_DIR = 1; + + /** The match code for an item in the Cheese table. */ + private static final int CODE_CHEESE_ITEM = 2; + + /** The URI matcher. */ + private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + + static { + MATCHER.addURI(AUTHORITY, Cheese.TABLE_NAME, CODE_CHEESE_DIR); + MATCHER.addURI(AUTHORITY, Cheese.TABLE_NAME + "/*", CODE_CHEESE_ITEM); + } + + @Override + public boolean onCreate() { + return true; + } + + @Nullable + @Override + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, + @Nullable String[] selectionArgs, @Nullable String sortOrder) { + final int code = MATCHER.match(uri); + if (code == CODE_CHEESE_DIR || code == CODE_CHEESE_ITEM) { + final Context context = getContext(); + if (context == null) { + return null; + } + CheeseDao cheese = SampleDatabase.getInstance(context).cheese(); + final Cursor cursor; + if (code == CODE_CHEESE_DIR) { + cursor = cheese.selectAll(); + } else { + cursor = cheese.selectById(ContentUris.parseId(uri)); + } + cursor.setNotificationUri(context.getContentResolver(), uri); + return cursor; + } else { + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + switch (MATCHER.match(uri)) { + case CODE_CHEESE_DIR: + return "vnd.android.cursor.dir/" + AUTHORITY + "." + Cheese.TABLE_NAME; + case CODE_CHEESE_ITEM: + return "vnd.android.cursor.item/" + AUTHORITY + "." + Cheese.TABLE_NAME; + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + switch (MATCHER.match(uri)) { + case CODE_CHEESE_DIR: + final Context context = getContext(); + if (context == null) { + return null; + } + final long id = SampleDatabase.getInstance(context).cheese() + .insert(Cheese.fromContentValues(values)); + context.getContentResolver().notifyChange(uri, null); + return ContentUris.withAppendedId(uri, id); + case CODE_CHEESE_ITEM: + throw new IllegalArgumentException("Invalid URI, cannot insert with ID: " + uri); + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String selection, + @Nullable String[] selectionArgs) { + switch (MATCHER.match(uri)) { + case CODE_CHEESE_DIR: + throw new IllegalArgumentException("Invalid URI, cannot update without ID" + uri); + case CODE_CHEESE_ITEM: + final Context context = getContext(); + if (context == null) { + return 0; + } + final int count = SampleDatabase.getInstance(context).cheese() + .deleteById(ContentUris.parseId(uri)); + context.getContentResolver().notifyChange(uri, null); + return count; + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + + @Override + public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, + @Nullable String[] selectionArgs) { + switch (MATCHER.match(uri)) { + case CODE_CHEESE_DIR: + throw new IllegalArgumentException("Invalid URI, cannot update without ID" + uri); + case CODE_CHEESE_ITEM: + final Context context = getContext(); + if (context == null) { + return 0; + } + final Cheese cheese = Cheese.fromContentValues(values); + cheese.id = ContentUris.parseId(uri); + final int count = SampleDatabase.getInstance(context).cheese() + .update(cheese); + context.getContentResolver().notifyChange(uri, null); + return count; + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + + @NonNull + @Override + public ContentProviderResult[] applyBatch( + @NonNull ArrayList operations) + throws OperationApplicationException { + final Context context = getContext(); + if (context == null) { + return new ContentProviderResult[0]; + } + final SampleDatabase database = SampleDatabase.getInstance(context); + database.beginTransaction(); + try { + final ContentProviderResult[] result = super.applyBatch(operations); + database.setTransactionSuccessful(); + return result; + } finally { + database.endTransaction(); + } + } + + @Override + public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] valuesArray) { + switch (MATCHER.match(uri)) { + case CODE_CHEESE_DIR: + final Context context = getContext(); + if (context == null) { + return 0; + } + final SampleDatabase database = SampleDatabase.getInstance(context); + final Cheese[] cheeses = new Cheese[valuesArray.length]; + for (int i = 0; i < valuesArray.length; i++) { + cheeses[i] = Cheese.fromContentValues(valuesArray[i]); + } + return database.cheese().insertAll(cheeses).length; + case CODE_CHEESE_ITEM: + throw new IllegalArgumentException("Invalid URI, cannot insert with ID: " + uri); + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + +} diff --git a/PersistenceContentProviderSample/app/src/main/res/layout/main_activity.xml b/PersistenceContentProviderSample/app/src/main/res/layout/main_activity.xml new file mode 100644 index 000000000..24592dd7b --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/res/layout/main_activity.xml @@ -0,0 +1,24 @@ + + + diff --git a/PersistenceContentProviderSample/app/src/main/res/mipmap-hdpi/ic_launcher.png b/PersistenceContentProviderSample/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..821d5103475008817b655f6c06a5866052be1e53 GIT binary patch literal 3284 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®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 literal 0 HcmV?d00001 diff --git a/PersistenceContentProviderSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/PersistenceContentProviderSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ca02616a4359224f688ea99a1dbb16a20ed9153b GIT binary patch literal 4145 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 literal 0 HcmV?d00001 diff --git a/PersistenceContentProviderSample/app/src/main/res/values/colors.xml b/PersistenceContentProviderSample/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..e17239428 --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/res/values/colors.xml @@ -0,0 +1,18 @@ + + + + #009688 + #00796B + #536DFE + diff --git a/PersistenceContentProviderSample/app/src/main/res/values/strings.xml b/PersistenceContentProviderSample/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..b2e4230bc --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/res/values/strings.xml @@ -0,0 +1,16 @@ + + + + ContentProvider Sample + diff --git a/PersistenceContentProviderSample/app/src/main/res/values/styles.xml b/PersistenceContentProviderSample/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..4a858f5a5 --- /dev/null +++ b/PersistenceContentProviderSample/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/PersistenceContentProviderSample/build.gradle b/PersistenceContentProviderSample/build.gradle new file mode 100644 index 000000000..24caa549c --- /dev/null +++ b/PersistenceContentProviderSample/build.gradle @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + maven { + url "https://android-devtools-staging.corp.google.com/no_crawl/maven2" + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/PersistenceContentProviderSample/gradle.properties b/PersistenceContentProviderSample/gradle.properties new file mode 100644 index 000000000..aac7c9b46 --- /dev/null +++ b/PersistenceContentProviderSample/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.jar b/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties b/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..c6e50609e --- /dev/null +++ b/PersistenceContentProviderSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Apr 21 15:24:08 JST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/PersistenceContentProviderSample/gradlew b/PersistenceContentProviderSample/gradlew new file mode 100755 index 000000000..9d82f7891 --- /dev/null +++ b/PersistenceContentProviderSample/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/PersistenceContentProviderSample/gradlew.bat b/PersistenceContentProviderSample/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/PersistenceContentProviderSample/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/PersistenceContentProviderSample/settings.gradle b/PersistenceContentProviderSample/settings.gradle new file mode 100644 index 000000000..e7b4def49 --- /dev/null +++ b/PersistenceContentProviderSample/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/README.md b/README.md new file mode 100644 index 000000000..3d7bde184 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +Android Architecture Components samples +=================================== + +A collection of samples using the Architecture Components: + +- Room +- Lifecycle-aware components +- ViewModels +- LiveData + +### Samples + +**[PersistenceBasicSample](https://github.com/googlesamples/android-architecture-components/blob/master/BasicPersistenceSample)** - Shows how to persist data using a SQLite database and Room. Also uses ViewModels and LiveData. + +**[PersistenceContentProviderSample](https://github.com/googlesamples/android-architecture-components/blob/master/PersistenceContentProviderSample)** - Shows how to expose data via a Content Provider using Room. + + +**[GithubBrowserSample](https://github.com/googlesamples/android-architecture-components/blob/master/AdvancedArchitectureSample)** - An **advanced** sample that uses the Architecture components, Dagger and the Github API. + + +Prerequisites +-------------- + +- TODO + +License +------- + +Copyright 2015 The Android Open Source Project, Inc. + +Licensed to the Apache Software Foundation (ASF) under one or more contributor +license agreements. See the NOTICE file distributed with this work for +additional information regarding copyright ownership. The ASF licenses this +file to you under the Apache License, Version 2.0 (the "License"); you may not +use this file except in compliance with the License. You may obtain a copy of +the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. + + From d7557fc3c1589072667550947ff1161c16a4341f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Alc=C3=A9rreca?= Date: Thu, 11 May 2017 19:19:10 +0200 Subject: [PATCH 003/110] Renames BasicSample and updates README Change-Id: I02b2a918d45bdce9a79b32ab5e1567946dffe0b4 --- .../.gitignore | 0 .../CONTRIBUTING.md | 0 {PersistenceBasicSample => BasicSample}/LICENSE | 0 .../app/build.gradle | 0 .../app/proguard-rules.pro | 0 .../android/persistence/MainActivityTest.java | 0 .../app/src/main/AndroidManifest.xml | 0 .../android/persistence/MainActivity.java | 0 .../android/persistence/ProductFragment.java | 0 .../android/persistence/ProductListFragment.java | 0 .../android/persistence/db/AppDatabase.java | 0 .../android/persistence/db/DatabaseCreator.java | 0 .../android/persistence/db/DatabaseInitUtil.java | 0 .../persistence/db/converter/DateConverter.java | 0 .../android/persistence/db/dao/CommentDao.java | 0 .../android/persistence/db/dao/ProductDao.java | 0 .../persistence/db/entity/CommentEntity.java | 0 .../persistence/db/entity/ProductEntity.java | 0 .../android/persistence/model/Comment.java | 0 .../android/persistence/model/Product.java | 0 .../android/persistence/ui/BindingAdapters.java | 0 .../android/persistence/ui/CommentAdapter.java | 0 .../persistence/ui/CommentClickCallback.java | 0 .../android/persistence/ui/ProductAdapter.java | 0 .../persistence/ui/ProductClickCallback.java | 0 .../viewmodel/ProductListViewModel.java | 0 .../persistence/viewmodel/ProductViewModel.java | 0 .../app/src/main/res/layout/comment_item.xml | 0 .../app/src/main/res/layout/list_fragment.xml | 0 .../app/src/main/res/layout/main_activity.xml | 0 .../app/src/main/res/layout/product_fragment.xml | 0 .../app/src/main/res/layout/product_item.xml | 0 .../app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values/colors.xml | 0 .../app/src/main/res/values/dimens.xml | 0 .../app/src/main/res/values/product_app.xml | 0 .../app/src/main/res/values/strings.xml | 0 .../app/src/main/res/values/styles.xml | 0 .../build.gradle | 0 .../gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 {PersistenceBasicSample => BasicSample}/gradlew | 0 .../gradlew.bat | 0 .../settings.gradle | 0 README.md | 15 +++++---------- 50 files changed, 5 insertions(+), 10 deletions(-) rename {PersistenceBasicSample => BasicSample}/.gitignore (100%) rename {PersistenceBasicSample => BasicSample}/CONTRIBUTING.md (100%) rename {PersistenceBasicSample => BasicSample}/LICENSE (100%) rename {PersistenceBasicSample => BasicSample}/app/build.gradle (100%) rename {PersistenceBasicSample => BasicSample}/app/proguard-rules.pro (100%) rename {PersistenceBasicSample => BasicSample}/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/AndroidManifest.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/MainActivity.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/ProductFragment.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/ProductListFragment.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/AppDatabase.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/model/Comment.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/model/Product.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/layout/comment_item.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/layout/list_fragment.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/layout/main_activity.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/layout/product_fragment.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/layout/product_item.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/values/colors.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/values/dimens.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/values/product_app.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/values/strings.xml (100%) rename {PersistenceBasicSample => BasicSample}/app/src/main/res/values/styles.xml (100%) rename {PersistenceBasicSample => BasicSample}/build.gradle (100%) rename {PersistenceBasicSample => BasicSample}/gradle.properties (100%) rename {PersistenceBasicSample => BasicSample}/gradle/wrapper/gradle-wrapper.jar (100%) rename {PersistenceBasicSample => BasicSample}/gradle/wrapper/gradle-wrapper.properties (100%) rename {PersistenceBasicSample => BasicSample}/gradlew (100%) rename {PersistenceBasicSample => BasicSample}/gradlew.bat (100%) rename {PersistenceBasicSample => BasicSample}/settings.gradle (100%) diff --git a/PersistenceBasicSample/.gitignore b/BasicSample/.gitignore similarity index 100% rename from PersistenceBasicSample/.gitignore rename to BasicSample/.gitignore diff --git a/PersistenceBasicSample/CONTRIBUTING.md b/BasicSample/CONTRIBUTING.md similarity index 100% rename from PersistenceBasicSample/CONTRIBUTING.md rename to BasicSample/CONTRIBUTING.md diff --git a/PersistenceBasicSample/LICENSE b/BasicSample/LICENSE similarity index 100% rename from PersistenceBasicSample/LICENSE rename to BasicSample/LICENSE diff --git a/PersistenceBasicSample/app/build.gradle b/BasicSample/app/build.gradle similarity index 100% rename from PersistenceBasicSample/app/build.gradle rename to BasicSample/app/build.gradle diff --git a/PersistenceBasicSample/app/proguard-rules.pro b/BasicSample/app/proguard-rules.pro similarity index 100% rename from PersistenceBasicSample/app/proguard-rules.pro rename to BasicSample/app/proguard-rules.pro diff --git a/PersistenceBasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java b/BasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java similarity index 100% rename from PersistenceBasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java rename to BasicSample/app/src/androidTest/java/com/example/android/persistence/MainActivityTest.java diff --git a/PersistenceBasicSample/app/src/main/AndroidManifest.xml b/BasicSample/app/src/main/AndroidManifest.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/AndroidManifest.xml rename to BasicSample/app/src/main/AndroidManifest.xml diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java b/BasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java rename to BasicSample/app/src/main/java/com/example/android/persistence/MainActivity.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java b/BasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java rename to BasicSample/app/src/main/java/com/example/android/persistence/ProductFragment.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java b/BasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java rename to BasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/AppDatabase.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseCreator.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/DatabaseInitUtil.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/converter/DateConverter.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/dao/CommentDao.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/dao/ProductDao.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/entity/CommentEntity.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java b/BasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java rename to BasicSample/app/src/main/java/com/example/android/persistence/db/entity/ProductEntity.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java b/BasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java rename to BasicSample/app/src/main/java/com/example/android/persistence/model/Comment.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Product.java b/BasicSample/app/src/main/java/com/example/android/persistence/model/Product.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/model/Product.java rename to BasicSample/app/src/main/java/com/example/android/persistence/model/Product.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/BindingAdapters.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/CommentAdapter.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/CommentClickCallback.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductAdapter.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java b/BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java rename to BasicSample/app/src/main/java/com/example/android/persistence/ui/ProductClickCallback.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java b/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java rename to BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java diff --git a/PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java b/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java similarity index 100% rename from PersistenceBasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java rename to BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductViewModel.java diff --git a/PersistenceBasicSample/app/src/main/res/layout/comment_item.xml b/BasicSample/app/src/main/res/layout/comment_item.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/layout/comment_item.xml rename to BasicSample/app/src/main/res/layout/comment_item.xml diff --git a/PersistenceBasicSample/app/src/main/res/layout/list_fragment.xml b/BasicSample/app/src/main/res/layout/list_fragment.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/layout/list_fragment.xml rename to BasicSample/app/src/main/res/layout/list_fragment.xml diff --git a/PersistenceBasicSample/app/src/main/res/layout/main_activity.xml b/BasicSample/app/src/main/res/layout/main_activity.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/layout/main_activity.xml rename to BasicSample/app/src/main/res/layout/main_activity.xml diff --git a/PersistenceBasicSample/app/src/main/res/layout/product_fragment.xml b/BasicSample/app/src/main/res/layout/product_fragment.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/layout/product_fragment.xml rename to BasicSample/app/src/main/res/layout/product_fragment.xml diff --git a/PersistenceBasicSample/app/src/main/res/layout/product_item.xml b/BasicSample/app/src/main/res/layout/product_item.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/layout/product_item.xml rename to BasicSample/app/src/main/res/layout/product_item.xml diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png b/BasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from PersistenceBasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to BasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-mdpi/ic_launcher.png b/BasicSample/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from PersistenceBasicSample/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to BasicSample/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/BasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from PersistenceBasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to BasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/BasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from PersistenceBasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to BasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/PersistenceBasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/BasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from PersistenceBasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to BasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/PersistenceBasicSample/app/src/main/res/values/colors.xml b/BasicSample/app/src/main/res/values/colors.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/values/colors.xml rename to BasicSample/app/src/main/res/values/colors.xml diff --git a/PersistenceBasicSample/app/src/main/res/values/dimens.xml b/BasicSample/app/src/main/res/values/dimens.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/values/dimens.xml rename to BasicSample/app/src/main/res/values/dimens.xml diff --git a/PersistenceBasicSample/app/src/main/res/values/product_app.xml b/BasicSample/app/src/main/res/values/product_app.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/values/product_app.xml rename to BasicSample/app/src/main/res/values/product_app.xml diff --git a/PersistenceBasicSample/app/src/main/res/values/strings.xml b/BasicSample/app/src/main/res/values/strings.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/values/strings.xml rename to BasicSample/app/src/main/res/values/strings.xml diff --git a/PersistenceBasicSample/app/src/main/res/values/styles.xml b/BasicSample/app/src/main/res/values/styles.xml similarity index 100% rename from PersistenceBasicSample/app/src/main/res/values/styles.xml rename to BasicSample/app/src/main/res/values/styles.xml diff --git a/PersistenceBasicSample/build.gradle b/BasicSample/build.gradle similarity index 100% rename from PersistenceBasicSample/build.gradle rename to BasicSample/build.gradle diff --git a/PersistenceBasicSample/gradle.properties b/BasicSample/gradle.properties similarity index 100% rename from PersistenceBasicSample/gradle.properties rename to BasicSample/gradle.properties diff --git a/PersistenceBasicSample/gradle/wrapper/gradle-wrapper.jar b/BasicSample/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from PersistenceBasicSample/gradle/wrapper/gradle-wrapper.jar rename to BasicSample/gradle/wrapper/gradle-wrapper.jar diff --git a/PersistenceBasicSample/gradle/wrapper/gradle-wrapper.properties b/BasicSample/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from PersistenceBasicSample/gradle/wrapper/gradle-wrapper.properties rename to BasicSample/gradle/wrapper/gradle-wrapper.properties diff --git a/PersistenceBasicSample/gradlew b/BasicSample/gradlew similarity index 100% rename from PersistenceBasicSample/gradlew rename to BasicSample/gradlew diff --git a/PersistenceBasicSample/gradlew.bat b/BasicSample/gradlew.bat similarity index 100% rename from PersistenceBasicSample/gradlew.bat rename to BasicSample/gradlew.bat diff --git a/PersistenceBasicSample/settings.gradle b/BasicSample/settings.gradle similarity index 100% rename from PersistenceBasicSample/settings.gradle rename to BasicSample/settings.gradle diff --git a/README.md b/README.md index 3d7bde184..cc53b9542 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,16 @@ A collection of samples using the Architecture Components: - Lifecycle-aware components - ViewModels - LiveData - -### Samples - -**[PersistenceBasicSample](https://github.com/googlesamples/android-architecture-components/blob/master/BasicPersistenceSample)** - Shows how to persist data using a SQLite database and Room. Also uses ViewModels and LiveData. - -**[PersistenceContentProviderSample](https://github.com/googlesamples/android-architecture-components/blob/master/PersistenceContentProviderSample)** - Shows how to expose data via a Content Provider using Room. +**The Architecture Components are not final and these samples are a preview** -**[GithubBrowserSample](https://github.com/googlesamples/android-architecture-components/blob/master/AdvancedArchitectureSample)** - An **advanced** sample that uses the Architecture components, Dagger and the Github API. +### Samples +**[BasicSample](https://github.com/googlesamples/android-architecture-components/blob/master/BasicSample)** - Shows how to persist data using a SQLite database and Room. Also uses ViewModels and LiveData. -Prerequisites --------------- +**[PersistenceContentProviderSample](https://github.com/googlesamples/android-architecture-components/blob/master/PersistenceContentProviderSample)** - Shows how to expose data via a Content Provider using Room. -- TODO +**[GithubBrowserSample](https://github.com/googlesamples/android-architecture-components/blob/master/AdvancedArchitectureSample)** - An **advanced** sample that uses the Architecture components, Dagger and the Github API. Requires Android Studio 2.4. License ------- From da70db4dd86e4640c6f903681780a3d56a4bbc68 Mon Sep 17 00:00:00 2001 From: Yigit Boyar Date: Sat, 13 May 2017 08:19:21 -0700 Subject: [PATCH 004/110] Initial commit for github demo Change-Id: I619f90214956d2de7587868dc8cac6bf6e322b61 --- GithubBrowserSample/.gitignore | 2 + GithubBrowserSample/README.md | 79 + GithubBrowserSample/app/.gitignore | 9 + GithubBrowserSample/app/build.gradle | 135 + GithubBrowserSample/app/proguard-rules.pro | 25 + .../com/android/example/github/TestApp.java | 31 + .../com/android/example/github/db/DbTest.java | 39 + .../example/github/db/RepoDaoTest.java | 115 + .../example/github/db/UserDaoTest.java | 48 + .../github/ui/repo/RepoFragmentTest.java | 189 ++ .../github/ui/search/SearchFragmentTest.java | 162 + .../github/ui/user/UserFragmentTest.java | 184 ++ .../example/github/util/GithubTestRunner.java | 34 + .../github/util/RecyclerViewMatcher.java | 89 + .../TaskExecutorWithIdlingResourceRule.java | 62 + .../example/github/util/ViewModelUtil.java | 38 + .../app/src/debug/AndroidManifest.xml | 25 + .../testing/SingleFragmentActivity.java | 47 + .../app/src/main/AndroidManifest.xml | 38 + .../android/example/github/AppExecutors.java | 75 + .../com/android/example/github/GithubApp.java | 49 + .../android/example/github/MainActivity.java | 59 + .../example/github/api/ApiResponse.java | 112 + .../example/github/api/GithubService.java | 53 + .../github/api/RepoSearchResponse.java | 72 + .../github/binding/BindingAdapters.java | 31 + .../binding/FragmentBindingAdapters.java | 41 + .../binding/FragmentDataBindingComponent.java | 36 + .../android/example/github/db/GithubDb.java | 38 + .../github/db/GithubTypeConverters.java | 38 + .../android/example/github/db/RepoDao.java | 94 + .../android/example/github/db/UserDao.java | 36 + .../example/github/di/AppComponent.java | 42 + .../example/github/di/AppInjector.java | 97 + .../android/example/github/di/AppModule.java | 70 + .../github/di/FragmentBuildersModule.java | 36 + .../android/example/github/di/Injectable.java | 23 + .../example/github/di/MainActivityModule.java | 28 + .../github/di/ViewModelSubComponent.java | 39 + .../repository/FetchNextSearchPageTask.java | 94 + .../repository/NetworkBoundResource.java | 110 + .../github/repository/RepoRepository.java | 230 ++ .../github/repository/UserRepository.java | 74 + .../ui/common/DataBoundListAdapter.java | 131 + .../github/ui/common/DataBoundViewHolder.java | 32 + .../ui/common/NavigationController.java | 65 + .../github/ui/common/RepoListAdapter.java | 79 + .../github/ui/common/RetryCallback.java | 24 + .../github/ui/repo/ContributorAdapter.java | 76 + .../example/github/ui/repo/RepoFragment.java | 132 + .../example/github/ui/repo/RepoViewModel.java | 122 + .../github/ui/search/SearchFragment.java | 161 + .../github/ui/search/SearchViewModel.java | 186 ++ .../example/github/ui/user/UserFragment.java | 100 + .../example/github/ui/user/UserViewModel.java | 81 + .../example/github/util/AbsentLiveData.java | 32 + .../example/github/util/AutoClearedValue.java | 44 + .../github/util/LiveDataCallAdapter.java | 70 + .../util/LiveDataCallAdapterFactory.java | 48 + .../android/example/github/util/Objects.java | 29 + .../example/github/util/RateLimiter.java | 56 + .../viewmodel/GithubViewModelFactory.java | 70 + .../example/github/vo/Contributor.java | 78 + .../com/android/example/github/vo/Repo.java | 92 + .../example/github/vo/RepoSearchResult.java | 43 + .../android/example/github/vo/Resource.java | 95 + .../com/android/example/github/vo/Status.java | 29 + .../com/android/example/github/vo/User.java | 47 + .../src/main/res/layout/contributor_item.xml | 60 + .../app/src/main/res/layout/loading_state.xml | 52 + .../app/src/main/res/layout/main_activity.xml | 26 + .../app/src/main/res/layout/repo_fragment.xml | 84 + .../app/src/main/res/layout/repo_item.xml | 67 + .../src/main/res/layout/search_fragment.xml | 95 + .../app/src/main/res/layout/user_fragment.xml | 84 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4208 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2555 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6114 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10056 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 14696 bytes .../app/src/main/res/values/colors.xml | 22 + .../app/src/main/res/values/dimens.xml | 21 + .../app/src/main/res/values/strings.xml | 27 + .../app/src/main/res/values/styles.xml | 27 + .../example/github/util/LiveDataTestUtil.java | 43 + .../android/example/github/util/TestUtil.java | 57 + .../example/github/api/ApiResponseTest.java | 89 + .../example/github/api/GithubServiceTest.java | 162 + .../FetchNextSearchPageTaskTest.java | 170 ++ .../repository/NetworkBoundResourceTest.java | 279 ++ .../github/repository/RepoRepositoryTest.java | 243 ++ .../github/repository/UserRepositoryTest.java | 95 + .../github/ui/repo/RepoViewModelTest.java | 153 + .../github/ui/search/NextPageHandlerTest.java | 159 + .../github/ui/search/SearchViewModelTest.java | 132 + .../github/ui/user/UserViewModelTest.java | 187 ++ .../android/example/github/util/ApiUtil.java | 44 + .../github/util/CountingAppExecutors.java | 104 + .../github/util/InstantAppExecutors.java | 29 + .../resources/api-response/contributors.json | 62 + .../resources/api-response/repos-yigit.json | 180 ++ .../test/resources/api-response/search.json | 2706 +++++++++++++++++ .../resources/api-response/user-yigit.json | 32 + GithubBrowserSample/build.gradle | 56 + GithubBrowserSample/gradle.properties | 33 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 22 + GithubBrowserSample/gradlew | 160 + GithubBrowserSample/gradlew.bat | 90 + GithubBrowserSample/settings.gradle | 17 + 115 files changed, 10719 insertions(+) create mode 100644 GithubBrowserSample/README.md create mode 100644 GithubBrowserSample/app/.gitignore create mode 100644 GithubBrowserSample/app/build.gradle create mode 100644 GithubBrowserSample/app/proguard-rules.pro create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/TestApp.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/DbTest.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/RepoDaoTest.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/UserDaoTest.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/GithubTestRunner.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/TaskExecutorWithIdlingResourceRule.java create mode 100644 GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/ViewModelUtil.java create mode 100644 GithubBrowserSample/app/src/debug/AndroidManifest.xml create mode 100644 GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java create mode 100644 GithubBrowserSample/app/src/main/AndroidManifest.xml create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/AppExecutors.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/GithubApp.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/api/GithubService.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/api/RepoSearchResponse.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/binding/BindingAdapters.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentBindingAdapters.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentDataBindingComponent.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubDb.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubTypeConverters.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/db/UserDao.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppComponent.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppInjector.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/di/FragmentBuildersModule.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/di/Injectable.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/di/MainActivityModule.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelSubComponent.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/repository/FetchNextSearchPageTask.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/repository/RepoRepository.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/repository/UserRepository.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundViewHolder.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/NavigationController.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RepoListAdapter.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RetryCallback.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/ContributorAdapter.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/util/AbsentLiveData.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapterFactory.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/util/Objects.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/util/RateLimiter.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Status.java create mode 100644 GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java create mode 100644 GithubBrowserSample/app/src/main/res/layout/contributor_item.xml create mode 100644 GithubBrowserSample/app/src/main/res/layout/loading_state.xml create mode 100644 GithubBrowserSample/app/src/main/res/layout/main_activity.xml create mode 100644 GithubBrowserSample/app/src/main/res/layout/repo_fragment.xml create mode 100644 GithubBrowserSample/app/src/main/res/layout/repo_item.xml create mode 100644 GithubBrowserSample/app/src/main/res/layout/search_fragment.xml create mode 100644 GithubBrowserSample/app/src/main/res/layout/user_fragment.xml create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 GithubBrowserSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 GithubBrowserSample/app/src/main/res/values/colors.xml create mode 100644 GithubBrowserSample/app/src/main/res/values/dimens.xml create mode 100644 GithubBrowserSample/app/src/main/res/values/strings.xml create mode 100644 GithubBrowserSample/app/src/main/res/values/styles.xml create mode 100644 GithubBrowserSample/app/src/test-common/java/com/android/example/github/util/LiveDataTestUtil.java create mode 100644 GithubBrowserSample/app/src/test-common/java/com/android/example/github/util/TestUtil.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/api/ApiResponseTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/api/GithubServiceTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/repository/FetchNextSearchPageTaskTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/repository/NetworkBoundResourceTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/repository/RepoRepositoryTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/repository/UserRepositoryTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/ui/repo/RepoViewModelTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/ui/search/NextPageHandlerTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/ui/search/SearchViewModelTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/ui/user/UserViewModelTest.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/util/ApiUtil.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/util/CountingAppExecutors.java create mode 100644 GithubBrowserSample/app/src/test/java/com/android/example/github/util/InstantAppExecutors.java create mode 100644 GithubBrowserSample/app/src/test/resources/api-response/contributors.json create mode 100644 GithubBrowserSample/app/src/test/resources/api-response/repos-yigit.json create mode 100644 GithubBrowserSample/app/src/test/resources/api-response/search.json create mode 100644 GithubBrowserSample/app/src/test/resources/api-response/user-yigit.json create mode 100644 GithubBrowserSample/build.gradle create mode 100644 GithubBrowserSample/gradle.properties create mode 100644 GithubBrowserSample/gradle/wrapper/gradle-wrapper.jar create mode 100644 GithubBrowserSample/gradle/wrapper/gradle-wrapper.properties create mode 100755 GithubBrowserSample/gradlew create mode 100644 GithubBrowserSample/gradlew.bat create mode 100644 GithubBrowserSample/settings.gradle diff --git a/GithubBrowserSample/.gitignore b/GithubBrowserSample/.gitignore index f4b27534b..273b7e937 100644 --- a/GithubBrowserSample/.gitignore +++ b/GithubBrowserSample/.gitignore @@ -6,3 +6,5 @@ build/ /captures .externalNativeBuild +app/build +build diff --git a/GithubBrowserSample/README.md b/GithubBrowserSample/README.md new file mode 100644 index 000000000..7d06247bc --- /dev/null +++ b/GithubBrowserSample/README.md @@ -0,0 +1,79 @@ +Github Browser Sample with Android Architecture Components + +This is a sample app that uses Android Architecture Components with Dagger 2. + +**NOTE** It is a relatively more complex and complete example so if you are not familiar +with [Architecture Components][arch], you are highly recommended to check other examples +in this repository first. + +## Functionality +The app is composed of 3 main screens. +### SearchFragment +Allows you to search repositories on Github. +Each search result is kept in the database in `RepoSearchResult` table where +the list of repository IDs are denormalized into a single column. +The actual `Repo` instances live in the `Repo` table. + +Each time a new page is fetched, the same `RepoSearchResult` record in the +Database is updated with the new list of repository ids. + +**NOTE** The UI currently loads all `Repo` items at once, which would not +perform well on lower end devices. Instead of manually writing lazy +adapters, we've decided to wait until the built in support in Room is released. + +### RepoFragment +This fragment displays the details of a repository and its contributors. +### UserFragment +This fragment displays a user and their repositories. + +## Building +You can open the project in Android studio and press run. +## Testing +The project uses both instrumentation tests that run on the device +and local unit tests that run on your computer. +To run both of them and generate a coverage report, you can run: + +`./gradlew fullCoverageReport` (requires a connected device or an emulator) + +### Device Tests +#### UI Tests +The projects uses Espresso for UI testing. Since each fragment +is limited to a ViewModel, each test mocks related ViewModel to +run the tests. +#### Database Tests +The project creates an in memory database for each database test but still +runs them on the device. + +### Local Unit Tests +#### ViewModel Tests +Each ViewModel is tested using local unit tests with mock Repository +implementations. +#### Repository Tests +Each Repository is tested using local unit tests with mock web service and +mock database. +#### Webservice Tests +The project uses [MockWebServer][mockwebserver] project to test REST api interactions. + + +## Libraries +* [Android Support Library][support-lib] +* [Android Architecture Components][arch] +* [Android Data Binding][data-binding] +* [Dagger 2][dagger2] for dependency injection +* [Retrofit][retrofit] for REST api communication +* [Glide][glide] for image loading +* [Timber][timber] for logging +* [espresso][espresso] for UI tests +* [mockito][mockito] for mocking in tests + + +[mockwebserver]: https://github.com/square/okhttp/tree/master/mockwebserver +[support-lib]: https://developer.android.com/topic/libraries/support-library/index.html +[arch]: https://developer.android.com/arch +[data-binding]: https://developer.android.com/topic/libraries/data-binding/index.html +[espresso]: https://google.github.io/android-testing-support-library/docs/espresso/ +[dagger2]: https://google.github.io/dagger +[retrofit]: http://square.github.io/retrofit +[glide]: https://github.com/bumptech/glide +[timber]: https://github.com/JakeWharton/timber +[mockito]: http://site.mockito.org diff --git a/GithubBrowserSample/app/.gitignore b/GithubBrowserSample/app/.gitignore new file mode 100644 index 000000000..473862d09 --- /dev/null +++ b/GithubBrowserSample/app/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/GithubBrowserSample/app/build.gradle b/GithubBrowserSample/app/build.gradle new file mode 100644 index 000000000..0d833118a --- /dev/null +++ b/GithubBrowserSample/app/build.gradle @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.application' +apply plugin: 'jacoco' +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "com.android.example.github" + minSdkVersion 14 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "com.android.example.github.util.GithubTestRunner" + } + buildTypes { + debug { + testCoverageEnabled !project.hasProperty('android.injected.invoked.from.ide') + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + dataBinding { + enabled = true + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + sourceSets { + androidTest.java.srcDirs += "src/test-common/java" + test.java.srcDirs += "src/test-common/java" + } + lintOptions { + disable 'GoogleAppIndexingWarning' + } +} + +jacoco { + toolVersion = "0.7.4+" +} + +dependencies { + compile "com.android.support:appcompat-v7:$support_version" + compile "com.android.support:recyclerview-v7:$support_version" + compile "com.android.support:cardview-v7:$support_version" + compile "com.android.support:design:$support_version" + compile "android.arch.persistence.room:runtime:$arch_version" + compile "android.arch.lifecycle:runtime:$arch_version" + compile "android.arch.lifecycle:extensions:$arch_version" + compile "com.squareup.retrofit2:retrofit:$retrofit_version" + compile "com.squareup.retrofit2:converter-gson:$retrofit_version" + compile "com.github.bumptech.glide:glide:$glide_version" + + compile "com.google.dagger:dagger:$dagger_version" + compile "com.google.dagger:dagger-android:$dagger_version" + compile "com.google.dagger:dagger-android-support:$dagger_version" + compile "com.android.support.constraint:constraint-layout:$constraint_layout_version" + + compile "com.jakewharton.timber:timber:$timber_version" + + annotationProcessor "com.google.dagger:dagger-android-processor:$dagger_version" + annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version" + annotationProcessor "android.arch.persistence.room:compiler:$arch_version" + annotationProcessor "android.arch.lifecycle:compiler:$arch_version" + + testCompile "junit:junit:$junit_version" + testCompile "com.squareup.okhttp3:mockwebserver:$mockwebserver_version" + testCompile ("android.arch.core:core-testing:$arch_version", { + exclude group: 'com.android.support', module: 'support-compat' + exclude group: 'com.android.support', module: 'support-annotations' + exclude group: 'com.android.support', module: 'support-core-utils' + }) + + androidTestCompile "com.android.support:appcompat-v7:$support_version" + androidTestCompile "com.android.support:recyclerview-v7:$support_version" + androidTestCompile "com.android.support:support-v4:$support_version" + androidTestCompile "com.android.support:design:$support_version" + + androidTestCompile("com.android.support.test.espresso:espresso-core:$espresso_version", { + exclude group: 'com.android.support', module: 'support-annotations' + exclude group: 'com.google.code.findbugs', module: 'jsr305' + }) + androidTestCompile("com.android.support.test.espresso:espresso-contrib:$espresso_version", { + exclude group: 'com.android.support', module: 'support-annotations' + exclude group: 'com.google.code.findbugs', module: 'jsr305' + }) + + androidTestCompile("android.arch.core:core-testing:$arch_version", { + }) + androidTestCompile "org.mockito:mockito-android:$mockito_version" +} + +task fullCoverageReport(type: JacocoReport) { + dependsOn 'createDebugCoverageReport' + dependsOn 'testDebugUnitTest' + reports { + xml.enabled = true + html.enabled = true + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', + '**/*Test*.*', 'android/**/*.*', + '**/*_MembersInjector.class', + '**/Dagger*Component.class', + '**/Dagger*Component$Builder.class', + '**/*_*Factory.class', + '**/*ComponentImpl.class', + '**/*SubComponentBuilder.class'] + def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter) + def mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories = files([mainSrc]) + classDirectories = files([debugTree]) + executionData = fileTree(dir: "$buildDir", includes: [ + "jacoco/testDebugUnitTest.exec", + "outputs/code-coverage/connected/*coverage.ec" + ]) +} diff --git a/GithubBrowserSample/app/proguard-rules.pro b/GithubBrowserSample/app/proguard-rules.pro new file mode 100644 index 000000000..4f3c5d10f --- /dev/null +++ b/GithubBrowserSample/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/yboyar/android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/TestApp.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/TestApp.java new file mode 100644 index 000000000..a62565e30 --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/TestApp.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github; + +import android.app.Application; + +/** + * We use a separate App for tests to prevent initializing dependency injection. + * + * See {@link com.android.example.github.util.GithubTestRunner}. + */ +public class TestApp extends Application { + @Override + public void onCreate() { + super.onCreate(); + } +} diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/DbTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/DbTest.java new file mode 100644 index 000000000..45e3b29f8 --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/DbTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.db; + + +import org.junit.After; +import org.junit.Before; + +import android.arch.persistence.room.Room; +import android.support.test.InstrumentationRegistry; + +abstract public class DbTest { + protected GithubDb db; + + @Before + public void initDb() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), + GithubDb.class).build(); + } + + @After + public void closeDb() { + db.close(); + } +} diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/RepoDaoTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/RepoDaoTest.java new file mode 100644 index 000000000..b958af3ab --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/RepoDaoTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.db; + +import com.android.example.github.util.TestUtil; +import com.android.example.github.vo.Contributor; +import com.android.example.github.vo.Repo; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.arch.lifecycle.LiveData; +import android.database.sqlite.SQLiteException; +import android.support.test.runner.AndroidJUnit4; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.android.example.github.util.LiveDataTestUtil.getValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@RunWith(AndroidJUnit4.class) +public class RepoDaoTest extends DbTest { + @Test + public void insertAndRead() throws InterruptedException { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + db.repoDao().insert(repo); + Repo loaded = getValue(db.repoDao().load("foo", "bar")); + assertThat(loaded, notNullValue()); + assertThat(loaded.name, is("bar")); + assertThat(loaded.description, is("desc")); + assertThat(loaded.owner, notNullValue()); + assertThat(loaded.owner.login, is("foo")); + } + + @Test + public void insertContributorsWithoutRepo() { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + Contributor contributor = TestUtil.createContributor(repo, "c1", 3); + try { + db.repoDao().insertContributors(Collections.singletonList(contributor)); + throw new AssertionError("must fail because repo does not exist"); + } catch (SQLiteException ex) {} + } + + @Test + public void insertContributors() throws InterruptedException { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + Contributor c1 = TestUtil.createContributor(repo, "c1", 3); + Contributor c2 = TestUtil.createContributor(repo, "c2", 7); + db.beginTransaction(); + try { + db.repoDao().insert(repo); + db.repoDao().insertContributors(Arrays.asList(c1, c2)); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + List list = getValue(db.repoDao().loadContributors("foo", "bar")); + assertThat(list.size(), is(2)); + Contributor first = list.get(0); + + assertThat(first.getLogin(), is("c2")); + assertThat(first.getContributions(), is(7)); + + Contributor second = list.get(1); + assertThat(second.getLogin(), is("c1")); + assertThat(second.getContributions(), is(3)); + } + + @Test + public void createIfNotExists_exists() throws InterruptedException { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + db.repoDao().insert(repo); + assertThat(db.repoDao().createRepoIfNotExists(repo), is(-1L)); + } + + @Test + public void createIfNotExists_doesNotExist() { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + assertThat(db.repoDao().createRepoIfNotExists(repo), is(1L)); + } + + @Test + public void insertContributorsThenUpdateRepo() throws InterruptedException { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + db.repoDao().insert(repo); + Contributor contributor = TestUtil.createContributor(repo, "aa", 3); + db.repoDao().insertContributors(Collections.singletonList(contributor)); + LiveData> data = db.repoDao().loadContributors("foo", "bar"); + assertThat(getValue(data).size(), is(1)); + + Repo update = TestUtil.createRepo("foo", "bar", "desc"); + db.repoDao().insert(update); + data = db.repoDao().loadContributors("foo", "bar"); + assertThat(getValue(data).size(), is(1)); + } +} diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/UserDaoTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/UserDaoTest.java new file mode 100644 index 000000000..75292d828 --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/db/UserDaoTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.db; + +import android.support.test.runner.AndroidJUnit4; + +import com.android.example.github.util.TestUtil; +import com.android.example.github.vo.User; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static com.android.example.github.util.LiveDataTestUtil.getValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@RunWith(AndroidJUnit4.class) +public class UserDaoTest extends DbTest { + + @Test + public void insertAndLoad() throws InterruptedException { + final User user = TestUtil.createUser("foo"); + db.userDao().insert(user); + + final User loaded = getValue(db.userDao().findByLogin(user.login)); + assertThat(loaded.login, is("foo")); + + final User replacement = TestUtil.createUser("foo2"); + db.userDao().insert(replacement); + + final User loadedReplacement = getValue(db.userDao().findByLogin("foo2")); + assertThat(loadedReplacement.login, is("foo2")); + } +} diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java new file mode 100644 index 000000000..b739934e6 --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/repo/RepoFragmentTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.repo; + +import com.android.example.github.R; +import com.android.example.github.binding.FragmentBindingAdapters; +import com.android.example.github.testing.SingleFragmentActivity; +import com.android.example.github.ui.common.NavigationController; +import com.android.example.github.util.RecyclerViewMatcher; +import com.android.example.github.util.TaskExecutorWithIdlingResourceRule; +import com.android.example.github.util.TestUtil; +import com.android.example.github.util.ViewModelUtil; +import com.android.example.github.vo.Contributor; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.Resource; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.arch.lifecycle.MutableLiveData; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import java.util.ArrayList; +import java.util.List; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class RepoFragmentTest { + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule<>(SingleFragmentActivity.class, true, true); + @Rule + public TaskExecutorWithIdlingResourceRule executorRule = + new TaskExecutorWithIdlingResourceRule(); + private MutableLiveData> repo = new MutableLiveData<>(); + private MutableLiveData>> contributors = new MutableLiveData<>(); + private RepoFragment repoFragment; + private RepoViewModel viewModel; + + private FragmentBindingAdapters fragmentBindingAdapters; + private NavigationController navigationController; + + + @Before + public void init() { + repoFragment = RepoFragment.create("a", "b"); + viewModel = mock(RepoViewModel.class); + fragmentBindingAdapters = mock(FragmentBindingAdapters.class); + navigationController = mock(NavigationController.class); + + when(viewModel.getRepo()).thenReturn(repo); + when(viewModel.getContributors()).thenReturn(contributors); + + repoFragment.viewModelFactory = ViewModelUtil.createFor(viewModel); + repoFragment.dataBindingComponent = () -> fragmentBindingAdapters; + repoFragment.navigationController = navigationController; + + activityRule.getActivity().setFragment(repoFragment); + } + + @Test + public void testLoading() { + repo.postValue(Resource.loading(null)); + onView(withId(R.id.progress_bar)).check(matches(isDisplayed())); + onView(withId(R.id.retry)).check(matches(not(isDisplayed()))); + } + + @Test + public void testValueWhileLoading() { + Repo repo = TestUtil.createRepo("yigit", "foo", "foo-bar"); + this.repo.postValue(Resource.loading(repo)); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + onView(withId(R.id.name)).check(matches( + withText(getString(R.string.repo_full_name, "yigit", "foo")))); + onView(withId(R.id.description)).check(matches(withText("foo-bar"))); + } + + @Test + public void testLoaded() throws InterruptedException { + Repo repo = TestUtil.createRepo("foo", "bar", "buzz"); + this.repo.postValue(Resource.success(repo)); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + onView(withId(R.id.name)).check(matches( + withText(getString(R.string.repo_full_name, "foo", "bar")))); + onView(withId(R.id.description)).check(matches(withText("buzz"))); + } + + @Test + public void testError() throws InterruptedException { + repo.postValue(Resource.error("foo", null)); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + onView(withId(R.id.retry)).check(matches(isDisplayed())); + onView(withId(R.id.retry)).perform(click()); + verify(viewModel).retry(); + repo.postValue(Resource.loading(null)); + + onView(withId(R.id.progress_bar)).check(matches(isDisplayed())); + onView(withId(R.id.retry)).check(matches(not(isDisplayed()))); + Repo repo = TestUtil.createRepo("owner", "name", "desc"); + this.repo.postValue(Resource.success(repo)); + + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + onView(withId(R.id.retry)).check(matches(not(isDisplayed()))); + onView(withId(R.id.name)).check(matches( + withText(getString(R.string.repo_full_name, "owner", "name")))); + onView(withId(R.id.description)).check(matches(withText("desc"))); + } + + @Test + public void testContributors() { + setContributors("aa", "bb"); + onView(listMatcher().atPosition(0)) + .check(matches(hasDescendant(withText("aa")))); + onView(listMatcher().atPosition(1)) + .check(matches(hasDescendant(withText("bb")))); + } + + @NonNull + private RecyclerViewMatcher listMatcher() { + return new RecyclerViewMatcher(R.id.contributor_list); + } + + @Test + public void testContributorClick() { + setContributors("aa", "bb", "cc"); + onView(withText("cc")).perform(click()); + verify(navigationController).navigateToUser("cc"); + } + + @Test + public void nullRepo() { + this.repo.postValue(null); + onView(withId(R.id.name)).check(matches(not(isDisplayed()))); + } + + @Test + public void nullContributors() { + setContributors("a", "b", "c"); + onView(listMatcher().atPosition(0)).check(matches(hasDescendant(withText("a")))); + contributors.postValue(null); + onView(listMatcher().atPosition(0)).check(doesNotExist()); + } + + private void setContributors(String... names) { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + List contributors = new ArrayList<>(); + int contributionCount = 100; + for (String name : names) { + contributors.add(TestUtil.createContributor(repo, name, contributionCount--)); + } + this.contributors.postValue(Resource.success(contributors)); + } + + private String getString(@StringRes int id, Object... args) { + return InstrumentationRegistry.getTargetContext().getString(id, args); + } +} \ No newline at end of file diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java new file mode 100644 index 000000000..a5435cf3e --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/search/SearchFragmentTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.search; + +import com.android.example.github.R; +import com.android.example.github.binding.FragmentBindingAdapters; +import com.android.example.github.testing.SingleFragmentActivity; +import com.android.example.github.ui.common.NavigationController; +import com.android.example.github.util.RecyclerViewMatcher; +import com.android.example.github.util.TaskExecutorWithIdlingResourceRule; +import com.android.example.github.util.TestUtil; +import com.android.example.github.util.ViewModelUtil; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.Resource; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.arch.lifecycle.MutableLiveData; +import android.support.annotation.NonNull; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.contrib.RecyclerViewActions; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.view.KeyEvent; + +import java.util.Arrays; +import java.util.List; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.pressKey; +import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class SearchFragmentTest { + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule<>(SingleFragmentActivity.class, true, true); + @Rule + public TaskExecutorWithIdlingResourceRule executorRule = + new TaskExecutorWithIdlingResourceRule(); + + private FragmentBindingAdapters fragmentBindingAdapters; + private NavigationController navigationController; + + private SearchViewModel viewModel; + + private MutableLiveData>> results = new MutableLiveData<>(); + private MutableLiveData loadMoreStatus = new MutableLiveData<>(); + + @Before + public void init() { + SearchFragment searchFragment = new SearchFragment(); + viewModel = mock(SearchViewModel.class); + when(viewModel.getLoadMoreStatus()).thenReturn(loadMoreStatus); + when(viewModel.getResults()).thenReturn(results); + + fragmentBindingAdapters = mock(FragmentBindingAdapters.class); + navigationController = mock(NavigationController.class); + searchFragment.viewModelFactory = ViewModelUtil.createFor(viewModel); + searchFragment.dataBindingComponent = () -> fragmentBindingAdapters; + searchFragment.navigationController = navigationController; + activityRule.getActivity().setFragment(searchFragment); + } + + @Test + public void search() { + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + onView(withId(R.id.input)).perform(typeText("foo"), + pressKey(KeyEvent.KEYCODE_ENTER)); + verify(viewModel).setQuery("foo"); + results.postValue(Resource.loading(null)); + onView(withId(R.id.progress_bar)).check(matches(isDisplayed())); + } + + @Test + public void loadResults() { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + results.postValue(Resource.success(Arrays.asList(repo))); + onView(listMatcher().atPosition(0)).check(matches(hasDescendant(withText("foo/bar")))); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + } + + @Test + public void dataWithLoading() { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + results.postValue(Resource.loading(Arrays.asList(repo))); + onView(listMatcher().atPosition(0)).check(matches(hasDescendant(withText("foo/bar")))); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + } + + @Test + public void error() { + results.postValue(Resource.error("failed to load", null)); + onView(withId(R.id.error_msg)).check(matches(isDisplayed())); + } + + @Test + public void loadMore() throws Throwable { + List repos = TestUtil.createRepos(50, "foo", "barr", "desc"); + results.postValue(Resource.success(repos)); + onView(withId(R.id.repo_list)).perform(RecyclerViewActions.scrollToPosition(49)); + onView(listMatcher().atPosition(49)).check(matches(isDisplayed())); + verify(viewModel).loadNextPage(); + } + + @Test + public void navigateToRepo() throws Throwable { + Repo repo = TestUtil.createRepo("foo", "bar", "desc"); + results.postValue(Resource.success(Arrays.asList(repo))); + onView(withText("desc")).perform(click()); + verify(navigationController).navigateToRepo("foo", "bar"); + } + + @Test + public void loadMoreProgress() { + loadMoreStatus.postValue(new SearchViewModel.LoadMoreState(true, null)); + onView(withId(R.id.load_more_bar)).check(matches(isDisplayed())); + loadMoreStatus.postValue(new SearchViewModel.LoadMoreState(false, null)); + onView(withId(R.id.load_more_bar)).check(matches(not(isDisplayed()))); + } + + @Test + public void loadMoreProgressError() { + loadMoreStatus.postValue(new SearchViewModel.LoadMoreState(true, "QQ")); + onView(withText("QQ")).check(matches( + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); + } + + @NonNull + private RecyclerViewMatcher listMatcher() { + return new RecyclerViewMatcher(R.id.repo_list); + } +} \ No newline at end of file diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java new file mode 100644 index 000000000..70bf2bf48 --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.user; + +import com.android.example.github.R; +import com.android.example.github.binding.FragmentBindingAdapters; +import com.android.example.github.testing.SingleFragmentActivity; +import com.android.example.github.ui.common.NavigationController; +import com.android.example.github.util.RecyclerViewMatcher; +import com.android.example.github.util.TestUtil; +import com.android.example.github.util.ViewModelUtil; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.Resource; +import com.android.example.github.vo.User; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.arch.lifecycle.MutableLiveData; +import android.support.annotation.NonNull; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import java.util.ArrayList; +import java.util.List; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class UserFragmentTest { + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule<>(SingleFragmentActivity.class, true, true); + + private UserViewModel viewModel; + private NavigationController navigationController; + private FragmentBindingAdapters fragmentBindingAdapters; + private MutableLiveData> userData = new MutableLiveData<>(); + private MutableLiveData>> repoListData = new MutableLiveData<>(); + + @Before + public void init() { + UserFragment fragment = UserFragment.create("foo"); + viewModel = mock(UserViewModel.class); + when(viewModel.getUser()).thenReturn(userData); + when(viewModel.getRepositories()).thenReturn(repoListData); + navigationController = mock(NavigationController.class); + fragmentBindingAdapters = mock(FragmentBindingAdapters.class); + + fragment.viewModelFactory = ViewModelUtil.createFor(viewModel); + fragment.navigationController = navigationController; + fragment.dataBindingComponent = () -> fragmentBindingAdapters; + + activityRule.getActivity().setFragment(fragment); + } + + @Test + public void loading() { + userData.postValue(Resource.loading(null)); + onView(withId(R.id.progress_bar)).check(matches(isDisplayed())); + onView(withId(R.id.retry)).check(matches(not(isDisplayed()))); + } + + @Test + public void error() { + userData.postValue(Resource.error("wtf", null)); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + onView(withId(R.id.error_msg)).check(matches(withText("wtf"))); + onView(withId(R.id.retry)).check(matches(isDisplayed())); + onView(withId(R.id.retry)).perform(click()); + verify(viewModel).retry(); + } + + @Test + public void loadingWithUser() { + User user = TestUtil.createUser("foo"); + userData.postValue(Resource.loading(user)); + onView(withId(R.id.name)).check(matches(withText(user.name))); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + } + + @Test + public void loadedUser() { + User user = TestUtil.createUser("foo"); + userData.postValue(Resource.success(user)); + onView(withId(R.id.name)).check(matches(withText(user.name))); + onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); + } + + @Test + public void loadRepos() { + List repos = setRepos(2); + for (int pos = 0; pos < repos.size(); pos ++) { + Repo repo = repos.get(pos); + onView(listMatcher().atPosition(pos)).check( + matches(hasDescendant(withText(repo.name)))); + onView(listMatcher().atPosition(pos)).check( + matches(hasDescendant(withText(repo.description)))); + onView(listMatcher().atPosition(pos)).check( + matches(hasDescendant(withText("" + repo.stars)))); + } + Repo repo3 = setRepos(3).get(2); + onView(listMatcher().atPosition(2)).check( + matches(hasDescendant(withText(repo3.name)))); + } + + @Test + public void clickRepo() { + List repos = setRepos(2); + Repo selected = repos.get(1); + onView(withText(selected.description)).perform(click()); + verify(navigationController).navigateToRepo(selected.owner.login, selected.name); + } + + @Test + public void nullUser() { + userData.postValue(null); + onView(withId(R.id.name)).check(matches(not(isDisplayed()))); + } + + @Test + public void nullRepoList() { + repoListData.postValue(null); + onView(listMatcher().atPosition(0)).check(doesNotExist()); + } + + @Test + public void nulledUser() { + User user = TestUtil.createUser("foo"); + userData.postValue(Resource.success(user)); + onView(withId(R.id.name)).check(matches(withText(user.name))); + userData.postValue(null); + onView(withId(R.id.name)).check(matches(not(isDisplayed()))); + } + + @Test + public void nulledRepoList() { + setRepos(5); + onView(listMatcher().atPosition(1)).check(matches(isDisplayed())); + repoListData.postValue(null); + onView(listMatcher().atPosition(0)).check(doesNotExist()); + } + + @NonNull + private RecyclerViewMatcher listMatcher() { + return new RecyclerViewMatcher(R.id.repo_list); + } + + private List setRepos(int count) { + List repos = new ArrayList<>(); + for (int i = 0; i < count; i++) { + repos.add(TestUtil.createRepo("foo", "name " + i, "desc" + i)); + } + repoListData.postValue(Resource.success(repos)); + return repos; + } +} \ No newline at end of file diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/GithubTestRunner.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/GithubTestRunner.java new file mode 100644 index 000000000..3525d03bd --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/GithubTestRunner.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import android.app.Application; +import android.content.Context; +import android.support.test.runner.AndroidJUnitRunner; + +import com.android.example.github.TestApp; + +/** + * Custom runner to disable dependency injection. + */ +public class GithubTestRunner extends AndroidJUnitRunner { + @Override + public Application newApplication(ClassLoader cl, String className, Context context) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return super.newApplication(cl, TestApp.class.getName(), context); + } +} diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java new file mode 100644 index 000000000..866028970 --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/RecyclerViewMatcher.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import android.content.res.Resources; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * taken from https://gist.github.com/baconpat/8405a88d04bd1942eb5e430d33e4faa2 + * license MIT + */ +public class RecyclerViewMatcher { + + private final int recyclerViewId; + + public RecyclerViewMatcher(int recyclerViewId) { + this.recyclerViewId = recyclerViewId; + } + + public Matcher atPosition(final int position) { + return atPositionOnView(position, -1); + } + + public Matcher atPositionOnView(final int position, final int targetViewId) { + + return new TypeSafeMatcher() { + Resources resources = null; + View childView; + + public void describeTo(Description description) { + String idDescription = Integer.toString(recyclerViewId); + if (this.resources != null) { + try { + idDescription = this.resources.getResourceName(recyclerViewId); + } catch (Resources.NotFoundException var4) { + idDescription = String.format("%s (resource name not found)", recyclerViewId); + } + } + + description.appendText("RecyclerView with id: " + idDescription + " at position: " + position); + } + + public boolean matchesSafely(View view) { + + this.resources = view.getResources(); + + if (childView == null) { + RecyclerView recyclerView = + (RecyclerView) view.getRootView().findViewById(recyclerViewId); + if (recyclerView != null && recyclerView.getId() == recyclerViewId) { + RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position); + if (viewHolder != null) { + childView = viewHolder.itemView; + } + } + else { + return false; + } + } + + if (targetViewId == -1) { + return view == childView; + } else { + View targetView = childView.findViewById(targetViewId); + return view == targetView; + } + } + }; + } +} \ No newline at end of file diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/TaskExecutorWithIdlingResourceRule.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/TaskExecutorWithIdlingResourceRule.java new file mode 100644 index 000000000..b9730d259 --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/TaskExecutorWithIdlingResourceRule.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import org.junit.runner.Description; + +import android.arch.core.executor.testing.CountingTaskExecutorRule; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.IdlingResource; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A Junit rule that registers Architecture Components' background threads as an Espresso idling + * resource. + */ +public class TaskExecutorWithIdlingResourceRule extends CountingTaskExecutorRule { + private CopyOnWriteArrayList callbacks = + new CopyOnWriteArrayList<>(); + @Override + protected void starting(Description description) { + Espresso.registerIdlingResources(new IdlingResource() { + @Override + public String getName() { + return "architecture components idling resource"; + } + + @Override + public boolean isIdleNow() { + return TaskExecutorWithIdlingResourceRule.this.isIdle(); + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback callback) { + callbacks.add(callback); + } + }); + super.starting(description); + } + + @Override + protected void onIdle() { + super.onIdle(); + for (IdlingResource.ResourceCallback callback : callbacks) { + callback.onTransitionToIdle(); + } + } +} diff --git a/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/ViewModelUtil.java b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/ViewModelUtil.java new file mode 100644 index 000000000..256aebc8b --- /dev/null +++ b/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/ViewModelUtil.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProvider; + +/** + * Creates a one off view model factory for the given view model instance. + */ +public class ViewModelUtil { + private ViewModelUtil() {} + public static ViewModelProvider.Factory createFor(T model) { + return new ViewModelProvider.Factory() { + @Override + public T create(Class modelClass) { + if (modelClass.isAssignableFrom(model.getClass())) { + return (T) model; + } + throw new IllegalArgumentException("unexpected model class " + modelClass); + } + }; + } +} diff --git a/GithubBrowserSample/app/src/debug/AndroidManifest.xml b/GithubBrowserSample/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..279b4e50a --- /dev/null +++ b/GithubBrowserSample/app/src/debug/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java b/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java new file mode 100644 index 000000000..a83a73658 --- /dev/null +++ b/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.testing; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.example.github.R; + +/** + * Used for testing fragments inside a fake activity. + */ +public class SingleFragmentActivity extends AppCompatActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FrameLayout content = new FrameLayout(this); + content.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + content.setId(R.id.container); + setContentView(content); + } + + public void setFragment(Fragment fragment) { + getSupportFragmentManager().beginTransaction() + .add(R.id.container, fragment, "TEST") + .commit(); + } +} diff --git a/GithubBrowserSample/app/src/main/AndroidManifest.xml b/GithubBrowserSample/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d827c96a6 --- /dev/null +++ b/GithubBrowserSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/AppExecutors.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/AppExecutors.java new file mode 100644 index 000000000..e366b1997 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/AppExecutors.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Global executor pools for the whole application. + *

+ * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind + * webservice requests). + */ +@Singleton +public class AppExecutors { + + private final Executor diskIO; + + private final Executor networkIO; + + private final Executor mainThread; + + public AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) { + this.diskIO = diskIO; + this.networkIO = networkIO; + this.mainThread = mainThread; + } + + @Inject + public AppExecutors() { + this(Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(3), + new MainThreadExecutor()); + } + + public Executor diskIO() { + return diskIO; + } + + public Executor networkIO() { + return networkIO; + } + + public Executor mainThread() { + return mainThread; + } + + private static class MainThreadExecutor implements Executor { + private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + @Override + public void execute(@NonNull Runnable command) { + mainThreadHandler.post(command); + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/GithubApp.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/GithubApp.java new file mode 100644 index 000000000..b30fc7417 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/GithubApp.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github; + +import com.android.example.github.di.AppInjector; + +import android.app.Activity; +import android.app.Application; + +import javax.inject.Inject; + +import dagger.android.DispatchingAndroidInjector; +import dagger.android.HasActivityInjector; +import timber.log.Timber; + + +public class GithubApp extends Application implements HasActivityInjector { + + @Inject + DispatchingAndroidInjector dispatchingAndroidInjector; + + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) { + Timber.plant(new Timber.DebugTree()); + } + AppInjector.init(this); + } + + @Override + public DispatchingAndroidInjector activityInjector() { + return dispatchingAndroidInjector; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java new file mode 100644 index 000000000..c82bd2ecf --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/MainActivity.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github; + +import com.android.example.github.ui.common.NavigationController; + +import android.arch.lifecycle.LifecycleRegistry; +import android.arch.lifecycle.LifecycleRegistryOwner; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; + +import javax.inject.Inject; + +import dagger.android.DispatchingAndroidInjector; +import dagger.android.support.HasSupportFragmentInjector; + +public class MainActivity extends AppCompatActivity implements LifecycleRegistryOwner, + HasSupportFragmentInjector { + private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this); + @Inject + DispatchingAndroidInjector dispatchingAndroidInjector; + @Inject + NavigationController navigationController; + + @Override + public LifecycleRegistry getLifecycle() { + return lifecycleRegistry; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + if (savedInstanceState == null) { + navigationController.navigateToSearch(); + } + } + + @Override + public DispatchingAndroidInjector supportFragmentInjector() { + return dispatchingAndroidInjector; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java new file mode 100644 index 000000000..33e352540 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/ApiResponse.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.api; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.util.ArrayMap; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import retrofit2.Response; +import timber.log.Timber; + +/** + * Common class used by API responses. + * @param + */ +public class ApiResponse { + private static final Pattern LINK_PATTERN = Pattern + .compile("<([^>]*)>[\\s]*;[\\s]*rel=\"([a-zA-Z0-9]+)\""); + private static final Pattern PAGE_PATTERN = Pattern.compile("page=(\\d)+"); + private static final String NEXT_LINK = "next"; + public final int code; + @Nullable + public final T body; + @Nullable + public final String errorMessage; + @NonNull + public final Map links; + + public ApiResponse(Throwable error) { + code = 500; + body = null; + errorMessage = error.getMessage(); + links = Collections.emptyMap(); + } + + public ApiResponse(Response response) { + code = response.code(); + if(response.isSuccessful()) { + body = response.body(); + errorMessage = null; + } else { + String message = null; + if (response.errorBody() != null) { + try { + message = response.errorBody().string(); + } catch (IOException ignored) { + Timber.e(ignored, "error while parsing response"); + } + } + if (message == null || message.trim().length() == 0) { + message = response.message(); + } + errorMessage = message; + body = null; + } + String linkHeader = response.headers().get("link"); + if (linkHeader == null) { + links = Collections.emptyMap(); + } else { + links = new ArrayMap<>(); + Matcher matcher = LINK_PATTERN.matcher(linkHeader); + + while (matcher.find()) { + int count = matcher.groupCount(); + if (count == 2) { + links.put(matcher.group(2), matcher.group(1)); + } + } + } + } + + public boolean isSuccessful() { + return code >= 200 && code < 300; + } + + public Integer getNextPage() { + String next = links.get(NEXT_LINK); + if (next == null) { + return null; + } + Matcher matcher = PAGE_PATTERN.matcher(next); + if (!matcher.find() || matcher.groupCount() != 1) { + return null; + } + try { + return Integer.parseInt(matcher.group(1)); + } catch (NumberFormatException ex) { + Timber.w("cannot parse next page from %s", next); + return null; + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/api/GithubService.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/GithubService.java new file mode 100644 index 000000000..8391dce5a --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/GithubService.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.api; + +import com.android.example.github.vo.Contributor; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.User; + +import android.arch.lifecycle.LiveData; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; +import retrofit2.http.Query; + +/** + * REST API access points + */ +public interface GithubService { + @GET("users/{login}") + LiveData> getUser(@Path("login") String login); + + @GET("users/{login}/repos") + LiveData>> getRepos(@Path("login") String login); + + @GET("repos/{owner}/{name}") + LiveData> getRepo(@Path("owner") String owner, @Path("name") String name); + + @GET("repos/{owner}/{name}/contributors") + LiveData>> getContributors(@Path("owner") String owner, @Path("name") String name); + + @GET("search/repositories") + LiveData> searchRepos(@Query("q") String query); + + @GET("search/repositories") + Call searchRepos(@Query("q") String query, @Query("page") int page); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/api/RepoSearchResponse.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/RepoSearchResponse.java new file mode 100644 index 000000000..d6108a8b4 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/api/RepoSearchResponse.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.api; + + +import com.google.gson.annotations.SerializedName; + +import com.android.example.github.vo.Repo; + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * POJO to hold repo search responses. This is different from the Entity in the database because + * we are keeping a search result in 1 row and denormalizing list of results into a single column. + */ +public class RepoSearchResponse { + @SerializedName("total_count") + private int total; + @SerializedName("items") + private List items; + private Integer nextPage; + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public void setNextPage(Integer nextPage) { + this.nextPage = nextPage; + } + + public Integer getNextPage() { + return nextPage; + } + + @NonNull + public List getRepoIds() { + List repoIds = new ArrayList<>(); + for (Repo repo : items) { + repoIds.add(repo.id); + } + return repoIds; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/BindingAdapters.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/BindingAdapters.java new file mode 100644 index 000000000..50054e5fd --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/BindingAdapters.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.binding; + +import android.databinding.BindingAdapter; +import android.view.View; +import android.widget.ImageView; + +/** + * Data Binding adapters specific to the app. + */ +public class BindingAdapters { + @BindingAdapter("visibleGone") + public static void showHide(View view, boolean show) { + view.setVisibility(show ? View.VISIBLE : View.GONE); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentBindingAdapters.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentBindingAdapters.java new file mode 100644 index 000000000..0bc8c4b2e --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentBindingAdapters.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.binding; + +import android.databinding.BindingAdapter; +import android.support.v4.app.Fragment; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; + +import javax.inject.Inject; + +/** + * Binding adapters that work with a fragment instance. + */ +public class FragmentBindingAdapters { + final Fragment fragment; + + @Inject + public FragmentBindingAdapters(Fragment fragment) { + this.fragment = fragment; + } + @BindingAdapter("imageUrl") + public void bindImage(ImageView imageView, String url) { + Glide.with(fragment).load(url).into(imageView); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentDataBindingComponent.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentDataBindingComponent.java new file mode 100644 index 000000000..d48de98e8 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/binding/FragmentDataBindingComponent.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.binding; + +import android.databinding.DataBindingComponent; +import android.support.v4.app.Fragment; + +/** + * A Data Binding Component implementation for fragments. + */ +public class FragmentDataBindingComponent implements DataBindingComponent { + private final FragmentBindingAdapters adapter; + + public FragmentDataBindingComponent(Fragment fragment) { + this.adapter = new FragmentBindingAdapters(fragment); + } + + @Override + public FragmentBindingAdapters getFragmentBindingAdapters() { + return adapter; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubDb.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubDb.java new file mode 100644 index 000000000..8e03d3acb --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubDb.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.db; + + +import com.android.example.github.vo.Contributor; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.RepoSearchResult; +import com.android.example.github.vo.User; + +import android.arch.persistence.room.Database; +import android.arch.persistence.room.RoomDatabase; + +/** + * Main database description. + */ +@Database(entities = {User.class, Repo.class, Contributor.class, + RepoSearchResult.class}, version = 3) +public abstract class GithubDb extends RoomDatabase { + + abstract public UserDao userDao(); + + abstract public RepoDao repoDao(); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubTypeConverters.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubTypeConverters.java new file mode 100644 index 000000000..72a474dda --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/GithubTypeConverters.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.db; + +import android.arch.persistence.room.TypeConverter; +import android.arch.persistence.room.util.StringUtil; + +import java.util.Collections; +import java.util.List; + +public class GithubTypeConverters { + @TypeConverter + public static List stringToIntList(String data) { + if (data == null) { + return Collections.emptyList(); + } + return StringUtil.splitToIntList(data); + } + + @TypeConverter + public static String intListToString(List ints) { + return StringUtil.joinIntoString(ints); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java new file mode 100644 index 000000000..ac4572bc1 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/RepoDao.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.db; + +import com.android.example.github.vo.Contributor; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.RepoSearchResult; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Transformations; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; +import android.arch.persistence.room.Query; +import android.arch.persistence.room.RoomWarnings; +import android.util.SparseIntArray; + +import java.util.Collections; +import java.util.List; + +/** + * Interface for database access on Repo related operations. + */ +@Dao +public abstract class RepoDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract void insert(Repo... repos); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract void insertContributors(List contributors); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract void insertRepos(List repositories); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + public abstract long createRepoIfNotExists(Repo repo); + + @Query("SELECT * FROM repo WHERE owner_login = :login AND name = :name") + public abstract LiveData load(String login, String name); + + @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) + @Query("SELECT login, avatarUrl, contributions FROM contributor " + + "WHERE repoName = :name AND repoOwner = :owner " + + "ORDER BY contributions DESC") + public abstract LiveData> loadContributors(String owner, String name); + + @Query("SELECT * FROM Repo " + + "WHERE owner_login = :owner " + + "ORDER BY stars DESC") + public abstract LiveData> loadRepositories(String owner); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract void insert(RepoSearchResult result); + + @Query("SELECT * FROM RepoSearchResult WHERE query = :query") + public abstract LiveData search(String query); + + public LiveData> loadOrdered(List repoIds) { + SparseIntArray order = new SparseIntArray(); + int index = 0; + for (Integer repoId : repoIds) { + order.put(repoId, index++); + } + return Transformations.map(loadById(repoIds), repositories -> { + Collections.sort(repositories, (r1, r2) -> { + int pos1 = order.get(r1.id); + int pos2 = order.get(r2.id); + return pos1 - pos2; + }); + return repositories; + }); + } + + @Query("SELECT * FROM Repo WHERE id in (:repoIds)") + protected abstract LiveData> loadById(List repoIds); + + @Query("SELECT * FROM RepoSearchResult WHERE query = :query") + public abstract RepoSearchResult findSearchResult(String query); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/db/UserDao.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/UserDao.java new file mode 100644 index 000000000..16c61054b --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/db/UserDao.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.db; + +import com.android.example.github.vo.User; +import android.arch.lifecycle.LiveData; +import android.arch.persistence.room.Dao; +import android.arch.persistence.room.Insert; +import android.arch.persistence.room.OnConflictStrategy; +import android.arch.persistence.room.Query; + +/** + * Interface for database access for User related operations. + */ +@Dao +public interface UserDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(User user); + + @Query("SELECT * FROM user WHERE login = :login") + LiveData findByLogin(String login); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppComponent.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppComponent.java new file mode 100644 index 000000000..88fb4ce94 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppComponent.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.di; + +import com.android.example.github.GithubApp; + +import android.app.Application; + +import javax.inject.Singleton; + +import dagger.BindsInstance; +import dagger.Component; +import dagger.android.AndroidInjectionModule; + +@Singleton +@Component(modules = { + AndroidInjectionModule.class, + AppModule.class, + MainActivityModule.class +}) +public interface AppComponent { + @Component.Builder + interface Builder { + @BindsInstance Builder application(Application application); + AppComponent build(); + } + void inject(GithubApp githubApp); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppInjector.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppInjector.java new file mode 100644 index 000000000..dff87a708 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppInjector.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.di; + +import com.android.example.github.GithubApp; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import dagger.android.AndroidInjection; +import dagger.android.support.AndroidSupportInjection; +import dagger.android.support.HasSupportFragmentInjector; + +/** + * Helper class to automatically inject fragments if they implement {@link Injectable}. + */ +public class AppInjector { + private AppInjector() {} + public static void init(GithubApp githubApp) { + DaggerAppComponent.builder().application(githubApp) + .build().inject(githubApp); + githubApp + .registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + handleActivity(activity); + } + + @Override + public void onActivityStarted(Activity activity) { + + } + + @Override + public void onActivityResumed(Activity activity) { + + } + + @Override + public void onActivityPaused(Activity activity) { + + } + + @Override + public void onActivityStopped(Activity activity) { + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + + } + + @Override + public void onActivityDestroyed(Activity activity) { + + } + }); + } + + private static void handleActivity(Activity activity) { + if (activity instanceof HasSupportFragmentInjector) { + AndroidInjection.inject(activity); + } + if (activity instanceof FragmentActivity) { + ((FragmentActivity) activity).getSupportFragmentManager() + .registerFragmentLifecycleCallbacks( + new FragmentManager.FragmentLifecycleCallbacks() { + @Override + public void onFragmentCreated(FragmentManager fm, Fragment f, + Bundle savedInstanceState) { + if (f instanceof Injectable) { + AndroidSupportInjection.inject(f); + } + } + }, true); + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java new file mode 100644 index 000000000..465b746ff --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/AppModule.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.di; + +import com.android.example.github.api.GithubService; +import com.android.example.github.db.GithubDb; +import com.android.example.github.db.RepoDao; +import com.android.example.github.db.UserDao; +import com.android.example.github.util.LiveDataCallAdapterFactory; +import com.android.example.github.viewmodel.GithubViewModelFactory; + +import android.app.Application; +import android.arch.lifecycle.ViewModelProvider; +import android.arch.persistence.room.Room; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +@Module(subcomponents = ViewModelSubComponent.class) +class AppModule { + @Singleton @Provides + GithubService provideGithubService() { + return new Retrofit.Builder() + .baseUrl("https://api.github.com/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(new LiveDataCallAdapterFactory()) + .build() + .create(GithubService.class); + } + + @Singleton @Provides + GithubDb provideDb(Application app) { + return Room.databaseBuilder(app, GithubDb.class,"github.db").build(); + } + + @Singleton @Provides + UserDao provideUserDao(GithubDb db) { + return db.userDao(); + } + + @Singleton @Provides + RepoDao provideRepoDao(GithubDb db) { + return db.repoDao(); + } + + @Singleton + @Provides + ViewModelProvider.Factory provideViewModelFactory( + ViewModelSubComponent.Builder viewModelSubComponent) { + return new GithubViewModelFactory(viewModelSubComponent.build()); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/FragmentBuildersModule.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/FragmentBuildersModule.java new file mode 100644 index 000000000..c90bf00f9 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/FragmentBuildersModule.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.di; + +import com.android.example.github.ui.repo.RepoFragment; +import com.android.example.github.ui.search.SearchFragment; +import com.android.example.github.ui.user.UserFragment; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; + +@Module +public abstract class FragmentBuildersModule { + @ContributesAndroidInjector + abstract RepoFragment contributeRepoFragment(); + + @ContributesAndroidInjector + abstract UserFragment contributeUserFragment(); + + @ContributesAndroidInjector + abstract SearchFragment contributeSearchFragment(); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/Injectable.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/Injectable.java new file mode 100644 index 000000000..fabebfe3a --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/Injectable.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.di; + +/** + * Marks an activity / fragment injectable. + */ +public interface Injectable { +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/MainActivityModule.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/MainActivityModule.java new file mode 100644 index 000000000..1a638b78c --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/MainActivityModule.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.di; + +import com.android.example.github.MainActivity; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; + +@Module +public abstract class MainActivityModule { + @ContributesAndroidInjector(modules = FragmentBuildersModule.class) + abstract MainActivity contributeMainActivity(); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelSubComponent.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelSubComponent.java new file mode 100644 index 000000000..81cde2180 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/di/ViewModelSubComponent.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.di; + +import com.android.example.github.ui.repo.RepoViewModel; +import com.android.example.github.ui.search.SearchViewModel; +import com.android.example.github.ui.user.UserViewModel; + +import dagger.Subcomponent; + +/** + * A sub component to create ViewModels. It is called by the + * {@link com.android.example.github.viewmodel.GithubViewModelFactory}. Using this component allows + * ViewModels to define {@link javax.inject.Inject} constructors. + */ +@Subcomponent +public interface ViewModelSubComponent { + @Subcomponent.Builder + interface Builder { + ViewModelSubComponent build(); + } + UserViewModel userViewModel(); + SearchViewModel searchViewModel(); + RepoViewModel repoViewModel(); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/FetchNextSearchPageTask.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/FetchNextSearchPageTask.java new file mode 100644 index 000000000..4d6ae92e2 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/FetchNextSearchPageTask.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.repository; + +import com.android.example.github.api.ApiResponse; +import com.android.example.github.api.GithubService; +import com.android.example.github.api.RepoSearchResponse; +import com.android.example.github.db.GithubDb; +import com.android.example.github.vo.RepoSearchResult; +import com.android.example.github.vo.Resource; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Response; + +/** + * A task that reads the search result in the database and fetches the next page, if it has one. + */ +public class FetchNextSearchPageTask implements Runnable { + private final MutableLiveData> liveData = new MutableLiveData<>(); + private final String query; + private final GithubService githubService; + private final GithubDb db; + + FetchNextSearchPageTask(String query, GithubService githubService, GithubDb db) { + this.query = query; + this.githubService = githubService; + this.db = db; + } + + @Override + public void run() { + RepoSearchResult current = db.repoDao().findSearchResult(query); + if(current == null) { + liveData.postValue(null); + return; + } + final Integer nextPage = current.next; + if (nextPage == null) { + liveData.postValue(Resource.success(false)); + return; + } + try { + Response response = githubService + .searchRepos(query, nextPage).execute(); + ApiResponse apiResponse = new ApiResponse<>(response); + if (apiResponse.isSuccessful()) { + // we merge all repo ids into 1 list so that it is easier to fetch the result list. + List ids = new ArrayList<>(); + ids.addAll(current.repoIds); + //noinspection ConstantConditions + ids.addAll(apiResponse.body.getRepoIds()); + RepoSearchResult merged = new RepoSearchResult(query, ids, + apiResponse.body.getTotal(), apiResponse.getNextPage()); + try { + db.beginTransaction(); + db.repoDao().insert(merged); + db.repoDao().insertRepos(apiResponse.body.getItems()); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + liveData.postValue(Resource.success(apiResponse.getNextPage() != null)); + } else { + liveData.postValue(Resource.error(apiResponse.errorMessage, true)); + } + } catch (IOException e) { + liveData.postValue(Resource.error(e.getMessage(), true)); + } + } + + LiveData> getLiveData() { + return liveData; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java new file mode 100644 index 000000000..e1dc8c797 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.repository; + +import com.android.example.github.AppExecutors; +import com.android.example.github.api.ApiResponse; +import com.android.example.github.vo.Resource; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MediatorLiveData; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +/** + * A generic class that can provide a resource backed by both the sqlite database and the network. + *

+ * These are usually created by the Repository classes where they return + * {@code LiveData>} to pass back the latest data to the UI with its fetch status. + */ +public enum Status { + SUCCESS, + ERROR, + LOADING +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java new file mode 100644 index 000000000..398d96c12 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/User.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.vo; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Index; +import com.google.gson.annotations.SerializedName; + +@Entity(primaryKeys = "login") +public class User { + @SerializedName("login") + public final String login; + @SerializedName("avatar_url") + public final String avatarUrl; + @SerializedName("name") + public final String name; + @SerializedName("company") + public final String company; + @SerializedName("repos_url") + public final String reposUrl; + @SerializedName("blog") + public final String blog; + + public User(String login, String avatarUrl, String name, String company, + String reposUrl, String blog) { + this.login = login; + this.avatarUrl = avatarUrl; + this.name = name; + this.company = company; + this.reposUrl = reposUrl; + this.blog = blog; + } +} diff --git a/GithubBrowserSample/app/src/main/res/layout/contributor_item.xml b/GithubBrowserSample/app/src/main/res/layout/contributor_item.xml new file mode 100644 index 000000000..a9ab11891 --- /dev/null +++ b/GithubBrowserSample/app/src/main/res/layout/contributor_item.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + diff --git a/GithubBrowserSample/app/src/main/res/layout/loading_state.xml b/GithubBrowserSample/app/src/main/res/layout/loading_state.xml new file mode 100644 index 000000000..5ef721dad --- /dev/null +++ b/GithubBrowserSample/app/src/main/res/layout/loading_state.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + +

+ * You can read more about it in the Architecture + * Guide. + * @param + * @param + */ +public abstract class NetworkBoundResource { + private final AppExecutors appExecutors; + + private final MediatorLiveData> result = new MediatorLiveData<>(); + + @MainThread + NetworkBoundResource(AppExecutors appExecutors) { + this.appExecutors = appExecutors; + result.setValue(Resource.loading(null)); + LiveData dbSource = loadFromDb(); + result.addSource(dbSource, data -> { + result.removeSource(dbSource); + if (shouldFetch(data)) { + fetchFromNetwork(dbSource); + } else { + result.addSource(dbSource, newData -> result.setValue(Resource.success(newData))); + } + }); + } + + private void fetchFromNetwork(final LiveData dbSource) { + LiveData> apiResponse = createCall(); + // we re-attach dbSource as a new source, it will dispatch its latest value quickly + result.addSource(dbSource, newData -> result.setValue(Resource.loading(newData))); + result.addSource(apiResponse, response -> { + result.removeSource(apiResponse); + result.removeSource(dbSource); + //noinspection ConstantConditions + if (response.isSuccessful()) { + appExecutors.diskIO().execute(() -> { + saveCallResult(processResponse(response)); + appExecutors.mainThread().execute(() -> + // we specially request a new live data, + // otherwise we will get immediately last cached value, + // which may not be updated with latest results received from network. + result.addSource(loadFromDb(), + newData -> result.setValue(Resource.success(newData))) + ); + }); + } else { + onFetchFailed(); + result.addSource(dbSource, + newData -> result.setValue(Resource.error(response.errorMessage, newData))); + } + }); + } + + protected void onFetchFailed() { + } + + public LiveData> asLiveData() { + return result; + } + + @WorkerThread + protected RequestType processResponse(ApiResponse response) { + return response.body; + } + + @WorkerThread + protected abstract void saveCallResult(@NonNull RequestType item); + + @MainThread + protected abstract boolean shouldFetch(@Nullable ResultType data); + + @NonNull + @MainThread + protected abstract LiveData loadFromDb(); + + @NonNull + @MainThread + protected abstract LiveData> createCall(); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/RepoRepository.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/RepoRepository.java new file mode 100644 index 000000000..38915f7eb --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/RepoRepository.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.repository; + +import com.android.example.github.AppExecutors; +import com.android.example.github.api.ApiResponse; +import com.android.example.github.api.GithubService; +import com.android.example.github.api.RepoSearchResponse; +import com.android.example.github.db.GithubDb; +import com.android.example.github.db.RepoDao; +import com.android.example.github.util.AbsentLiveData; +import com.android.example.github.util.RateLimiter; +import com.android.example.github.vo.Contributor; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.RepoSearchResult; +import com.android.example.github.vo.Resource; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Transformations; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import timber.log.Timber; + +/** + * Repository that handles Repo instances. + * + * unfortunate naming :/ . + * Repo - value object name + * Repository - type of this class. + */ +@Singleton +public class RepoRepository { + + private final GithubDb db; + + private final RepoDao repoDao; + + private final GithubService githubService; + + private final AppExecutors appExecutors; + + private RateLimiter repoListRateLimit = new RateLimiter<>(10, TimeUnit.MINUTES); + + @Inject + public RepoRepository(AppExecutors appExecutors, GithubDb db, RepoDao repoDao, + GithubService githubService) { + this.db = db; + this.repoDao = repoDao; + this.githubService = githubService; + this.appExecutors = appExecutors; + } + + public LiveData>> loadRepos(String owner) { + return new NetworkBoundResource, List>(appExecutors) { + @Override + protected void saveCallResult(@NonNull List item) { + repoDao.insertRepos(item); + } + + @Override + protected boolean shouldFetch(@Nullable List data) { + return data == null || data.isEmpty() || repoListRateLimit.shouldFetch(owner); + } + + @NonNull + @Override + protected LiveData> loadFromDb() { + return repoDao.loadRepositories(owner); + } + + @NonNull + @Override + protected LiveData>> createCall() { + return githubService.getRepos(owner); + } + + @Override + protected void onFetchFailed() { + repoListRateLimit.reset(owner); + } + }.asLiveData(); + } + + public LiveData> loadRepo(String owner, String name) { + return new NetworkBoundResource(appExecutors) { + @Override + protected void saveCallResult(@NonNull Repo item) { + repoDao.insert(item); + } + + @Override + protected boolean shouldFetch(@Nullable Repo data) { + return data == null; + } + + @NonNull + @Override + protected LiveData loadFromDb() { + return repoDao.load(owner, name); + } + + @NonNull + @Override + protected LiveData> createCall() { + return githubService.getRepo(owner, name); + } + }.asLiveData(); + } + + public LiveData>> loadContributors(String owner, String name) { + return new NetworkBoundResource, List>(appExecutors) { + @Override + protected void saveCallResult(@NonNull List contributors) { + for (Contributor contributor : contributors) { + contributor.setRepoName(name); + contributor.setRepoOwner(owner); + } + db.beginTransaction(); + try { + repoDao.createRepoIfNotExists(new Repo(Repo.UNKNOWN_ID, + name, owner + "/" + name, "", + new Repo.Owner(owner, null), 0)); + repoDao.insertContributors(contributors); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + Timber.d("rece saved contributors to db"); + } + + @Override + protected boolean shouldFetch(@Nullable List data) { + Timber.d("rece contributor list from db: %s", data); + return data == null || data.isEmpty(); + } + + @NonNull + @Override + protected LiveData> loadFromDb() { + return repoDao.loadContributors(owner, name); + } + + @NonNull + @Override + protected LiveData>> createCall() { + return githubService.getContributors(owner, name); + } + }.asLiveData(); + } + + public LiveData> searchNextPage(String query) { + FetchNextSearchPageTask fetchNextSearchPageTask = new FetchNextSearchPageTask( + query, githubService, db); + appExecutors.networkIO().execute(fetchNextSearchPageTask); + return fetchNextSearchPageTask.getLiveData(); + } + + public LiveData>> search(String query) { + return new NetworkBoundResource, RepoSearchResponse>(appExecutors) { + + @Override + protected void saveCallResult(@NonNull RepoSearchResponse item) { + List repoIds = item.getRepoIds(); + RepoSearchResult repoSearchResult = new RepoSearchResult( + query, repoIds, item.getTotal(), item.getNextPage()); + db.beginTransaction(); + try { + repoDao.insertRepos(item.getItems()); + repoDao.insert(repoSearchResult); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + @Override + protected boolean shouldFetch(@Nullable List data) { + return data == null; + } + + @NonNull + @Override + protected LiveData> loadFromDb() { + return Transformations.switchMap(repoDao.search(query), searchData -> { + if (searchData == null) { + return AbsentLiveData.create(); + } else { + return repoDao.loadOrdered(searchData.repoIds); + } + }); + } + + @NonNull + @Override + protected LiveData> createCall() { + return githubService.searchRepos(query); + } + + @Override + protected RepoSearchResponse processResponse(ApiResponse response) { + RepoSearchResponse body = response.body; + if (body != null) { + body.setNextPage(response.getNextPage()); + } + return body; + } + }.asLiveData(); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/UserRepository.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/UserRepository.java new file mode 100644 index 000000000..2f0431a4b --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/UserRepository.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.repository; + +import com.android.example.github.AppExecutors; +import com.android.example.github.api.ApiResponse; +import com.android.example.github.api.GithubService; +import com.android.example.github.db.UserDao; +import com.android.example.github.vo.Resource; +import com.android.example.github.vo.User; + +import android.arch.lifecycle.LiveData; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Repository that handles User objects. + */ +@Singleton +public class UserRepository { + private final UserDao userDao; + private final GithubService githubService; + private final AppExecutors appExecutors; + + @Inject + UserRepository(AppExecutors appExecutors, UserDao userDao, GithubService githubService) { + this.userDao = userDao; + this.githubService = githubService; + this.appExecutors = appExecutors; + } + + public LiveData> loadUser(String login) { + return new NetworkBoundResource(appExecutors) { + @Override + protected void saveCallResult(@NonNull User item) { + userDao.insert(item); + } + + @Override + protected boolean shouldFetch(@Nullable User data) { + return data == null; + } + + @NonNull + @Override + protected LiveData loadFromDb() { + return userDao.findByLogin(login); + } + + @NonNull + @Override + protected LiveData> createCall() { + return githubService.getUser(login); + } + }.asLiveData(); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java new file mode 100644 index 000000000..3e78b503a --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundListAdapter.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.common; + +import android.annotation.SuppressLint; +import android.databinding.ViewDataBinding; +import android.os.AsyncTask; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.v7.util.DiffUtil; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import java.util.List; + +/** + * A generic RecyclerView adapter that uses Data Binding & DiffUtil. + * + * @param Type of the items in the list + * @param The of the ViewDataBinding + */ +public abstract class DataBoundListAdapter + extends RecyclerView.Adapter> { + + @Nullable + private List items; + // each time data is set, we update this variable so that if DiffUtil calculation returns + // after repetitive updates, we can ignore the old calculation + private int dataVersion = 0; + + @Override + public final DataBoundViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + V binding = createBinding(parent); + return new DataBoundViewHolder<>(binding); + } + + protected abstract V createBinding(ViewGroup parent); + + @Override + public final void onBindViewHolder(DataBoundViewHolder holder, int position) { + //noinspection ConstantConditions + bind(holder.binding, items.get(position)); + holder.binding.executePendingBindings(); + } + + @SuppressLint("StaticFieldLeak") + @MainThread + public void replace(List update) { + dataVersion ++; + if (items == null) { + if (update == null) { + return; + } + items = update; + notifyDataSetChanged(); + } else if (update == null) { + int oldSize = items.size(); + items = null; + notifyItemRangeRemoved(0, oldSize); + } else { + final int startVersion = dataVersion; + final List oldItems = items; + new AsyncTask() { + @Override + protected DiffUtil.DiffResult doInBackground(Void... voids) { + return DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return oldItems.size(); + } + + @Override + public int getNewListSize() { + return update.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + T oldItem = oldItems.get(oldItemPosition); + T newItem = update.get(newItemPosition); + return DataBoundListAdapter.this.areItemsTheSame(oldItem, newItem); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + T oldItem = oldItems.get(oldItemPosition); + T newItem = update.get(newItemPosition); + return DataBoundListAdapter.this.areContentsTheSame(oldItem, newItem); + } + }); + } + + @Override + protected void onPostExecute(DiffUtil.DiffResult diffResult) { + if (startVersion != dataVersion) { + // ignore update + return; + } + items = update; + diffResult.dispatchUpdatesTo(DataBoundListAdapter.this); + + } + }.execute(); + } + } + + protected abstract void bind(V binding, T item); + + protected abstract boolean areItemsTheSame(T oldItem, T newItem); + + protected abstract boolean areContentsTheSame(T oldItem, T newItem); + + @Override + public int getItemCount() { + return items == null ? 0 : items.size(); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundViewHolder.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundViewHolder.java new file mode 100644 index 000000000..3b154d779 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/DataBoundViewHolder.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.common; + +import android.databinding.ViewDataBinding; +import android.support.v7.widget.RecyclerView; + +/** + * A generic ViewHolder that works with a {@link ViewDataBinding}. + * @param The type of the ViewDataBinding. + */ +public class DataBoundViewHolder extends RecyclerView.ViewHolder { + public final T binding; + DataBoundViewHolder(T binding) { + super(binding.getRoot()); + this.binding = binding; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/NavigationController.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/NavigationController.java new file mode 100644 index 000000000..0c2f1fc6e --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/NavigationController.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.common; + +import com.android.example.github.MainActivity; +import com.android.example.github.R; +import com.android.example.github.ui.repo.RepoFragment; +import com.android.example.github.ui.search.SearchFragment; +import com.android.example.github.ui.user.UserFragment; + +import android.support.v4.app.FragmentManager; + +import javax.inject.Inject; + +/** + * A utility class that handles navigation in {@link MainActivity}. + */ +public class NavigationController { + private final int containerId; + private final FragmentManager fragmentManager; + @Inject + public NavigationController(MainActivity mainActivity) { + this.containerId = R.id.container; + this.fragmentManager = mainActivity.getSupportFragmentManager(); + } + + public void navigateToSearch() { + SearchFragment searchFragment = new SearchFragment(); + fragmentManager.beginTransaction() + .replace(containerId, searchFragment) + .commitAllowingStateLoss(); + } + + public void navigateToRepo(String owner, String name) { + RepoFragment fragment = RepoFragment.create(owner, name); + String tag = "repo" + "/" + owner + "/" + name; + fragmentManager.beginTransaction() + .replace(containerId, fragment, tag) + .addToBackStack(null) + .commitAllowingStateLoss(); + } + + public void navigateToUser(String login) { + String tag = "user" + "/" + login; + UserFragment userFragment = UserFragment.create(login); + fragmentManager.beginTransaction() + .replace(containerId, userFragment, tag) + .addToBackStack(null) + .commitAllowingStateLoss(); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RepoListAdapter.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RepoListAdapter.java new file mode 100644 index 000000000..e105ccf82 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RepoListAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.common; + +import com.android.example.github.R; +import com.android.example.github.databinding.RepoItemBinding; +import com.android.example.github.util.Objects; +import com.android.example.github.vo.Repo; + +import android.databinding.DataBindingComponent; +import android.databinding.DataBindingUtil; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +/** + * A RecyclerView adapter for {@link Repo} class. + */ +public class RepoListAdapter extends DataBoundListAdapter { + private final DataBindingComponent dataBindingComponent; + private final RepoClickCallback repoClickCallback; + private final boolean showFullName; + + public RepoListAdapter(DataBindingComponent dataBindingComponent, boolean showFullName, + RepoClickCallback repoClickCallback) { + this.dataBindingComponent = dataBindingComponent; + this.repoClickCallback = repoClickCallback; + this.showFullName = showFullName; + } + + @Override + protected RepoItemBinding createBinding(ViewGroup parent) { + RepoItemBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), R.layout.repo_item, + parent, false, dataBindingComponent); + binding.setShowFullName(showFullName); + binding.getRoot().setOnClickListener(v -> { + Repo repo = binding.getRepo(); + if (repo != null && repoClickCallback != null) { + repoClickCallback.onClick(repo); + } + }); + return binding; + } + + @Override + protected void bind(RepoItemBinding binding, Repo item) { + binding.setRepo(item); + } + + @Override + protected boolean areItemsTheSame(Repo oldItem, Repo newItem) { + return Objects.equals(oldItem.owner, newItem.owner) && + Objects.equals(oldItem.name, newItem.name); + } + + @Override + protected boolean areContentsTheSame(Repo oldItem, Repo newItem) { + return Objects.equals(oldItem.description, newItem.description) && + oldItem.stars == newItem.stars; + } + + public interface RepoClickCallback { + void onClick(Repo repo); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RetryCallback.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RetryCallback.java new file mode 100644 index 000000000..237204615 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/common/RetryCallback.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.common; + +/** + * Generic interface for retry buttons. + */ +public interface RetryCallback { + void retry(); +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/ContributorAdapter.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/ContributorAdapter.java new file mode 100644 index 000000000..162c70241 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/ContributorAdapter.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.repo; + +import com.android.example.github.R; +import com.android.example.github.databinding.ContributorItemBinding; +import com.android.example.github.ui.common.DataBoundListAdapter; +import com.android.example.github.util.Objects; +import com.android.example.github.vo.Contributor; + +import android.databinding.DataBindingComponent; +import android.databinding.DataBindingUtil; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +public class ContributorAdapter + extends DataBoundListAdapter { + + private final DataBindingComponent dataBindingComponent; + private final ContributorClickCallback callback; + + public ContributorAdapter(DataBindingComponent dataBindingComponent, + ContributorClickCallback callback) { + this.dataBindingComponent = dataBindingComponent; + this.callback = callback; + } + + @Override + protected ContributorItemBinding createBinding(ViewGroup parent) { + ContributorItemBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), + R.layout.contributor_item, parent, false, + dataBindingComponent); + binding.getRoot().setOnClickListener(v -> { + Contributor contributor = binding.getContributor(); + if (contributor != null && callback != null) { + callback.onClick(contributor); + } + }); + return binding; + } + + @Override + protected void bind(ContributorItemBinding binding, Contributor item) { + binding.setContributor(item); + } + + @Override + protected boolean areItemsTheSame(Contributor oldItem, Contributor newItem) { + return Objects.equals(oldItem.getLogin(), newItem.getLogin()); + } + + @Override + protected boolean areContentsTheSame(Contributor oldItem, Contributor newItem) { + return Objects.equals(oldItem.getAvatarUrl(), newItem.getAvatarUrl()) + && oldItem.getContributions() == newItem.getContributions(); + } + + public interface ContributorClickCallback { + void onClick(Contributor contributor); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java new file mode 100644 index 000000000..014231e9a --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.repo; + +import com.android.example.github.R; +import com.android.example.github.binding.FragmentDataBindingComponent; +import com.android.example.github.databinding.RepoFragmentBinding; +import com.android.example.github.di.Injectable; +import com.android.example.github.ui.common.NavigationController; +import com.android.example.github.util.AutoClearedValue; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.Resource; + +import android.arch.lifecycle.LifecycleRegistry; +import android.arch.lifecycle.LifecycleRegistryOwner; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.databinding.DataBindingComponent; +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Collections; + +import javax.inject.Inject; + +/** + * The UI Controller for displaying a Github Repo's information with its contributors. + */ +public class RepoFragment extends Fragment implements LifecycleRegistryOwner, Injectable { + + private static final String REPO_OWNER_KEY = "repo_owner"; + + private static final String REPO_NAME_KEY = "repo_name"; + + private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this); + + @Inject + ViewModelProvider.Factory viewModelFactory; + + private RepoViewModel repoViewModel; + + @Inject + NavigationController navigationController; + + DataBindingComponent dataBindingComponent = new FragmentDataBindingComponent(this); + AutoClearedValue binding; + AutoClearedValue adapter; + + @Override + public LifecycleRegistry getLifecycle() { + return lifecycleRegistry; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + repoViewModel = ViewModelProviders.of(this, viewModelFactory).get(RepoViewModel.class); + Bundle args = getArguments(); + if (args != null && args.containsKey(REPO_OWNER_KEY) && + args.containsKey(REPO_NAME_KEY)) { + repoViewModel.setId(args.getString(REPO_OWNER_KEY), + args.getString(REPO_NAME_KEY)); + } else { + repoViewModel.setId(null, null); + } + LiveData> repo = repoViewModel.getRepo(); + repo.observe(this, resource -> { + binding.get().setRepo(resource == null ? null : resource.data); + binding.get().setRepoResource(resource); + binding.get().executePendingBindings(); + }); + + ContributorAdapter adapter = new ContributorAdapter(dataBindingComponent, + contributor -> navigationController.navigateToUser(contributor.getLogin())); + this.adapter = new AutoClearedValue<>(this, adapter); + binding.get().contributorList.setAdapter(adapter); + initContributorList(repoViewModel); + } + + private void initContributorList(RepoViewModel viewModel) { + viewModel.getContributors().observe(this, listResource -> { + // we don't need any null checks here for the adapter since LiveData guarantees that + // it won't call us if fragment is stopped or not started. + if (listResource != null && listResource.data != null) { + adapter.get().replace(listResource.data); + } else { + //noinspection ConstantConditions + adapter.get().replace(Collections.emptyList()); + } + }); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + RepoFragmentBinding dataBinding = DataBindingUtil + .inflate(inflater, R.layout.repo_fragment, container, false); + dataBinding.setRetryCallback(() -> repoViewModel.retry()); + binding = new AutoClearedValue<>(this, dataBinding); + return dataBinding.getRoot(); + } + + public static RepoFragment create(String owner, String name) { + RepoFragment repoFragment = new RepoFragment(); + Bundle args = new Bundle(); + args.putString(REPO_OWNER_KEY, owner); + args.putString(REPO_NAME_KEY, name); + repoFragment.setArguments(args); + return repoFragment; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java new file mode 100644 index 000000000..4c980600e --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoViewModel.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.repo; + +import com.android.example.github.repository.RepoRepository; +import com.android.example.github.util.AbsentLiveData; +import com.android.example.github.util.Objects; +import com.android.example.github.vo.Contributor; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.Resource; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Transformations; +import android.arch.lifecycle.ViewModel; +import android.support.annotation.VisibleForTesting; + +import java.util.List; + +import javax.inject.Inject; + +public class RepoViewModel extends ViewModel { + @VisibleForTesting + final MutableLiveData repoId; + private final LiveData> repo; + private final LiveData>> contributors; + + @Inject + public RepoViewModel(RepoRepository repository) { + this.repoId = new MutableLiveData<>(); + repo = Transformations.switchMap(repoId, input -> { + if (input.isEmpty()) { + return AbsentLiveData.create(); + } + return repository.loadRepo(input.owner, input.name); + }); + contributors = Transformations.switchMap(repoId, input -> { + if (input.isEmpty()) { + return AbsentLiveData.create(); + } else { + return repository.loadContributors(input.owner, input.name); + } + + }); + } + + public LiveData> getRepo() { + return repo; + } + + public LiveData>> getContributors() { + return contributors; + } + + public void retry() { + RepoId current = repoId.getValue(); + if (current != null && !current.isEmpty()) { + repoId.setValue(current); + } + } + + void setId(String owner, String name) { + RepoId update = new RepoId(owner, name); + if (Objects.equals(repoId.getValue(), update)) { + return; + } + repoId.setValue(update); + } + + @VisibleForTesting + static class RepoId { + public final String owner; + public final String name; + + RepoId(String owner, String name) { + this.owner = owner == null ? null : owner.trim(); + this.name = name == null ? null : name.trim(); + } + + boolean isEmpty() { + return owner == null || name == null || owner.length() == 0 || name.length() == 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RepoId repoId = (RepoId) o; + + if (owner != null ? !owner.equals(repoId.owner) : repoId.owner != null) { + return false; + } + return name != null ? name.equals(repoId.name) : repoId.name == null; + } + + @Override + public int hashCode() { + int result = owner != null ? owner.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java new file mode 100644 index 000000000..701fcaa4c --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchFragment.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.search; + +import com.android.example.github.R; +import com.android.example.github.binding.FragmentDataBindingComponent; +import com.android.example.github.databinding.SearchFragmentBinding; +import com.android.example.github.di.Injectable; +import com.android.example.github.ui.common.NavigationController; +import com.android.example.github.ui.common.RepoListAdapter; +import com.android.example.github.util.AutoClearedValue; + +import android.arch.lifecycle.LifecycleFragment; +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; +import android.databinding.DataBindingComponent; +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.support.design.widget.Snackbar; +import android.support.v4.app.FragmentActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; + +import javax.inject.Inject; + +public class SearchFragment extends LifecycleFragment implements Injectable { + + @Inject + ViewModelProvider.Factory viewModelFactory; + + @Inject + NavigationController navigationController; + + DataBindingComponent dataBindingComponent = new FragmentDataBindingComponent(this); + + AutoClearedValue binding; + + AutoClearedValue adapter; + + private SearchViewModel searchViewModel; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + SearchFragmentBinding dataBinding = DataBindingUtil + .inflate(inflater, R.layout.search_fragment, container, false, + dataBindingComponent); + binding = new AutoClearedValue<>(this, dataBinding); + return dataBinding.getRoot(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + searchViewModel = ViewModelProviders.of(this, viewModelFactory).get(SearchViewModel.class); + initRecyclerView(); + RepoListAdapter rvAdapter = new RepoListAdapter(dataBindingComponent, true, + repo -> navigationController.navigateToRepo(repo.owner.login, repo.name)); + binding.get().repoList.setAdapter(rvAdapter); + adapter = new AutoClearedValue<>(this, rvAdapter); + + initSearchInputListener(); + + binding.get().setCallback(() -> searchViewModel.refresh()); + } + + private void initSearchInputListener() { + binding.get().input.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + doSearch(v); + return true; + } + return false; + }); + binding.get().input.setOnKeyListener((v, keyCode, event) -> { + if ((event.getAction() == KeyEvent.ACTION_DOWN) + && (keyCode == KeyEvent.KEYCODE_ENTER)) { + doSearch(v); + return true; + } + return false; + }); + } + + private void doSearch(View v) { + String query = binding.get().input.getText().toString(); + // Dismiss keyboard + dismissKeyboard(v.getWindowToken()); + binding.get().setQuery(query); + searchViewModel.setQuery(query); + } + + private void initRecyclerView() { + + binding.get().repoList.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + LinearLayoutManager layoutManager = (LinearLayoutManager) + recyclerView.getLayoutManager(); + int lastPosition = layoutManager + .findLastVisibleItemPosition(); + if (lastPosition == adapter.get().getItemCount() - 1) { + searchViewModel.loadNextPage(); + } + } + }); + searchViewModel.getResults().observe(this, result -> { + binding.get().setSearchResource(result); + binding.get().setResultCount((result == null || result.data == null) + ? 0 : result.data.size()); + adapter.get().replace(result == null ? null : result.data); + binding.get().executePendingBindings(); + }); + + searchViewModel.getLoadMoreStatus().observe(this, loadingMore -> { + if (loadingMore == null) { + binding.get().setLoadingMore(false); + } else { + binding.get().setLoadingMore(loadingMore.isRunning()); + String error = loadingMore.getErrorMessageIfNotHandled(); + if (error != null) { + Snackbar.make(binding.get().loadMoreBar, error, Snackbar.LENGTH_LONG).show(); + } + } + binding.get().executePendingBindings(); + }); + } + + private void dismissKeyboard(IBinder windowToken) { + FragmentActivity activity = getActivity(); + if (activity != null) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(windowToken, 0); + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java new file mode 100644 index 000000000..b5f684bb6 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/search/SearchViewModel.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.search; + +import com.android.example.github.repository.RepoRepository; +import com.android.example.github.util.AbsentLiveData; +import com.android.example.github.util.Objects; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.Resource; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.Transformations; +import android.arch.lifecycle.ViewModel; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import java.util.List; +import java.util.Locale; + +import javax.inject.Inject; + +public class SearchViewModel extends ViewModel { + + private final MutableLiveData query = new MutableLiveData<>(); + + private final LiveData>> results; + + private final NextPageHandler nextPageHandler; + + @Inject + SearchViewModel(RepoRepository repoRepository) { + nextPageHandler = new NextPageHandler(repoRepository); + results = Transformations.switchMap(query, search -> { + if (search == null || search.trim().length() == 0) { + return AbsentLiveData.create(); + } else { + return repoRepository.search(search); + } + }); + } + + LiveData>> getResults() { + return results; + } + + public void setQuery(@NonNull String originalInput) { + String input = originalInput.toLowerCase(Locale.getDefault()).trim(); + if (Objects.equals(input, query.getValue())) { + return; + } + nextPageHandler.reset(); + query.setValue(input); + } + + LiveData getLoadMoreStatus() { + return nextPageHandler.getLoadMoreState(); + } + + void loadNextPage() { + String value = query.getValue(); + if (value == null || value.trim().length() == 0) { + return; + } + nextPageHandler.queryNextPage(value); + } + + void refresh() { + if (query.getValue() != null) { + query.setValue(query.getValue()); + } + } + + static class LoadMoreState { + private final boolean running; + private final String errorMessage; + private boolean handledError = false; + + LoadMoreState(boolean running, String errorMessage) { + this.running = running; + this.errorMessage = errorMessage; + } + + boolean isRunning() { + return running; + } + + String getErrorMessage() { + return errorMessage; + } + + String getErrorMessageIfNotHandled() { + if (handledError) { + return null; + } + handledError = true; + return errorMessage; + } + } + + @VisibleForTesting + static class NextPageHandler implements Observer> { + @Nullable + private LiveData> nextPageLiveData; + private final MutableLiveData loadMoreState = new MutableLiveData<>(); + private String query; + private final RepoRepository repository; + @VisibleForTesting + boolean hasMore; + + @VisibleForTesting + NextPageHandler(RepoRepository repository) { + this.repository = repository; + reset(); + } + + void queryNextPage(String query) { + if (Objects.equals(this.query, query)) { + return; + } + unregister(); + this.query = query; + nextPageLiveData = repository.searchNextPage(query); + loadMoreState.setValue(new LoadMoreState(true, null)); + //noinspection ConstantConditions + nextPageLiveData.observeForever(this); + } + + @Override + public void onChanged(@Nullable Resource result) { + if (result == null) { + reset(); + } else { + switch (result.status) { + case SUCCESS: + hasMore = Boolean.TRUE.equals(result.data); + unregister(); + loadMoreState.setValue(new LoadMoreState(false, null)); + break; + case ERROR: + hasMore = true; + unregister(); + loadMoreState.setValue(new LoadMoreState(false, + result.message)); + break; + } + } + } + + private void unregister() { + if (nextPageLiveData != null) { + nextPageLiveData.removeObserver(this); + nextPageLiveData = null; + if (hasMore) { + query = null; + } + } + } + + private void reset() { + unregister(); + hasMore = true; + loadMoreState.setValue(new LoadMoreState(false, null)); + } + + MutableLiveData getLoadMoreState() { + return loadMoreState; + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java new file mode 100644 index 000000000..33fca94b6 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserFragment.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.user; + +import com.android.example.github.R; +import com.android.example.github.binding.FragmentDataBindingComponent; +import com.android.example.github.databinding.UserFragmentBinding; +import com.android.example.github.di.Injectable; +import com.android.example.github.ui.common.NavigationController; +import com.android.example.github.ui.common.RepoListAdapter; +import com.android.example.github.util.AutoClearedValue; + +import android.arch.lifecycle.LifecycleFragment; +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.databinding.DataBindingComponent; +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import javax.inject.Inject; + +public class UserFragment extends LifecycleFragment implements Injectable { + private static final String LOGIN_KEY = "login"; + @Inject + ViewModelProvider.Factory viewModelFactory; + @Inject + NavigationController navigationController; + + DataBindingComponent dataBindingComponent = new FragmentDataBindingComponent(this); + private UserViewModel userViewModel; + private AutoClearedValue binding; + private AutoClearedValue adapter; + + public static UserFragment create(String login) { + UserFragment userFragment = new UserFragment(); + Bundle bundle = new Bundle(); + bundle.putString(LOGIN_KEY, login); + userFragment.setArguments(bundle); + return userFragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + UserFragmentBinding dataBinding = DataBindingUtil.inflate(inflater, R.layout.user_fragment, + container, false, dataBindingComponent); + dataBinding.setRetryCallback(() -> userViewModel.retry()); + binding = new AutoClearedValue<>(this, dataBinding); + return dataBinding.getRoot(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + userViewModel = ViewModelProviders.of(this, viewModelFactory).get(UserViewModel.class); + userViewModel.setLogin(getArguments().getString(LOGIN_KEY)); + userViewModel.getUser().observe(this, userResource -> { + binding.get().setUser(userResource == null ? null : userResource.data); + binding.get().setUserResource(userResource); + // this is only necessary because espresso cannot read data binding callbacks. + binding.get().executePendingBindings(); + }); + RepoListAdapter rvAdapter = new RepoListAdapter(dataBindingComponent, false, + repo -> navigationController.navigateToRepo(repo.owner.login, repo.name)); + binding.get().repoList.setAdapter(rvAdapter); + this.adapter = new AutoClearedValue<>(this, rvAdapter); + initRepoList(); + } + + private void initRepoList() { + userViewModel.getRepositories().observe(this, repos -> { + // no null checks for adapter.get() since LiveData guarantees that we'll not receive + // the event if fragment is now show. + if (repos == null) { + adapter.get().replace(null); + } else { + adapter.get().replace(repos.data); + } + }); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java new file mode 100644 index 000000000..8a9a00e93 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/user/UserViewModel.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.ui.user; + +import com.android.example.github.repository.RepoRepository; +import com.android.example.github.repository.UserRepository; +import com.android.example.github.util.AbsentLiveData; +import com.android.example.github.util.Objects; +import com.android.example.github.vo.Repo; +import com.android.example.github.vo.Resource; +import com.android.example.github.vo.User; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.Transformations; +import android.arch.lifecycle.ViewModel; +import android.support.annotation.VisibleForTesting; + +import java.util.List; + +import javax.inject.Inject; + +public class UserViewModel extends ViewModel { + @VisibleForTesting + final MutableLiveData login = new MutableLiveData<>(); + private final LiveData>> repositories; + private final LiveData> user; + @SuppressWarnings("unchecked") + @Inject + public UserViewModel(UserRepository userRepository, RepoRepository repoRepository) { + user = Transformations.switchMap(login, login -> { + if (login == null) { + return AbsentLiveData.create(); + } else { + return userRepository.loadUser(login); + } + }); + repositories = Transformations.switchMap(login, login -> { + if (login == null) { + return AbsentLiveData.create(); + } else { + return repoRepository.loadRepos(login); + } + }); + } + + void setLogin(String login) { + if (Objects.equals(this.login.getValue(), login)) { + return; + } + this.login.setValue(login); + } + + LiveData> getUser() { + return user; + } + + LiveData>> getRepositories() { + return repositories; + } + + void retry() { + if (this.login.getValue() != null) { + this.login.setValue(this.login.getValue()); + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AbsentLiveData.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AbsentLiveData.java new file mode 100644 index 000000000..c497a1ef5 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AbsentLiveData.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import android.arch.lifecycle.LiveData; + +/** + * A LiveData class that has {@code null} value. + */ +public class AbsentLiveData extends LiveData { + private AbsentLiveData() { + postValue(null); + } + public static LiveData create() { + //noinspection unchecked + return new AbsentLiveData(); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java new file mode 100644 index 000000000..001ce2ea9 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/AutoClearedValue.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; + +/** + * A value holder that automatically clears the reference if the Fragment's view is destroyed. + * @param + */ +public class AutoClearedValue { + private T value; + public AutoClearedValue(Fragment fragment, T value) { + FragmentManager fragmentManager = fragment.getFragmentManager(); + fragmentManager.registerFragmentLifecycleCallbacks( + new FragmentManager.FragmentLifecycleCallbacks() { + @Override + public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) { + AutoClearedValue.this.value = null; + fragmentManager.unregisterFragmentLifecycleCallbacks(this); + } + },false); + this.value = value; + } + + public T get() { + return value; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java new file mode 100644 index 000000000..a22a553dd --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapter.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + + +import com.android.example.github.api.ApiResponse; + +import android.arch.lifecycle.LiveData; + +import java.lang.reflect.Type; +import java.util.concurrent.atomic.AtomicBoolean; + +import retrofit2.Call; +import retrofit2.CallAdapter; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * A Retrofit adapterthat converts the Call into a LiveData of ApiResponse. + * @param + */ +public class LiveDataCallAdapter implements CallAdapter>> { + private final Type responseType; + public LiveDataCallAdapter(Type responseType) { + this.responseType = responseType; + } + + @Override + public Type responseType() { + return responseType; + } + + @Override + public LiveData> adapt(Call call) { + return new LiveData>() { + AtomicBoolean started = new AtomicBoolean(false); + @Override + protected void onActive() { + super.onActive(); + if (started.compareAndSet(false, true)) { + call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + postValue(new ApiResponse<>(response)); + } + + @Override + public void onFailure(Call call, Throwable throwable) { + postValue(new ApiResponse(throwable)); + } + }); + } + } + }; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapterFactory.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapterFactory.java new file mode 100644 index 000000000..f9afac9f4 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapterFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import com.android.example.github.api.ApiResponse; + +import android.arch.lifecycle.LiveData; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import retrofit2.CallAdapter; +import retrofit2.Retrofit; + +public class LiveDataCallAdapterFactory extends CallAdapter.Factory { + + @Override + public CallAdapter get(Type returnType, Annotation[] annotations, Retrofit retrofit) { + if (getRawType(returnType) != LiveData.class) { + return null; + } + Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType); + Class rawObservableType = getRawType(observableType); + if (rawObservableType != ApiResponse.class) { + throw new IllegalArgumentException("type must be a resource"); + } + if (! (observableType instanceof ParameterizedType)) { + throw new IllegalArgumentException("resource must be parameterized"); + } + Type bodyType = getParameterUpperBound(0, (ParameterizedType) observableType); + return new LiveDataCallAdapter<>(bodyType); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/Objects.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/Objects.java new file mode 100644 index 000000000..f78c29536 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/Objects.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +public class Objects { + public static boolean equals(Object o1, Object o2) { + if (o1 == null) { + return o2 == null; + } + if (o2 == null) { + return false; + } + return o1.equals(o2); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/util/RateLimiter.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/RateLimiter.java new file mode 100644 index 000000000..77c0e2717 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/util/RateLimiter.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.util; + +import android.os.SystemClock; +import android.support.v4.util.ArrayMap; + +import java.util.concurrent.TimeUnit; + +/** + * Utility class that decides whether we should fetch some data or not. + */ +public class RateLimiter { + private ArrayMap timestamps = new ArrayMap<>(); + private final long timeout; + + public RateLimiter(int timeout, TimeUnit timeUnit) { + this.timeout = timeUnit.toMillis(timeout); + } + + public synchronized boolean shouldFetch(KEY key) { + Long lastFetched = timestamps.get(key); + long now = now(); + if (lastFetched == null) { + timestamps.put(key, now); + return true; + } + if (now - lastFetched > timeout) { + timestamps.put(key, now); + return true; + } + return false; + } + + private long now() { + return SystemClock.uptimeMillis(); + } + + public synchronized void reset(KEY key) { + timestamps.remove(key); + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java new file mode 100644 index 000000000..64bbde8be --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/viewmodel/GithubViewModelFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.viewmodel; + +import com.android.example.github.di.ViewModelSubComponent; +import com.android.example.github.repository.RepoRepository; +import com.android.example.github.repository.UserRepository; +import com.android.example.github.ui.repo.RepoViewModel; +import com.android.example.github.ui.search.SearchViewModel; +import com.android.example.github.ui.user.UserViewModel; + +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProvider; +import android.support.v4.util.ArrayMap; + +import java.util.Map; +import java.util.concurrent.Callable; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class GithubViewModelFactory implements ViewModelProvider.Factory { + private final ArrayMap> creators; + + @Inject + public GithubViewModelFactory(ViewModelSubComponent viewModelSubComponent) { + creators = new ArrayMap<>(); + // we cannot inject view models directly because they won't be bound to the owner's + // view model scope. + creators.put(SearchViewModel.class, () -> viewModelSubComponent.searchViewModel()); + creators.put(UserViewModel.class, () -> viewModelSubComponent.userViewModel()); + creators.put(RepoViewModel.class, () -> viewModelSubComponent.repoViewModel()); + } + + @Override + public T create(Class modelClass) { + Callable creator = creators.get(modelClass); + if (creator == null) { + for (Map.Entry> entry : creators.entrySet()) { + if (modelClass.isAssignableFrom(entry.getKey())) { + creator = entry.getValue(); + break; + } + } + } + if (creator == null) { + throw new IllegalArgumentException("unknown model class " + modelClass); + } + try { + return (T) creator.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java new file mode 100644 index 000000000..d8f1a0dae --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Contributor.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.vo; + +import com.google.gson.annotations.SerializedName; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.ForeignKey; + +@Entity(primaryKeys = {"repoName", "repoOwner", "login"}, + foreignKeys = @ForeignKey(entity = Repo.class, + parentColumns = {"name", "owner_login"}, + childColumns = {"repoName", "repoOwner"}, + onUpdate = ForeignKey.CASCADE, + deferred = true)) +public class Contributor { + + @SerializedName("login") + private final String login; + + @SerializedName("contributions") + private final int contributions; + + @SerializedName("avatar_url") + private final String avatarUrl; + + private String repoName; + + private String repoOwner; + + public Contributor(String login, int contributions, String avatarUrl) { + this.login = login; + this.contributions = contributions; + this.avatarUrl = avatarUrl; + } + + public void setRepoName(String repoName) { + this.repoName = repoName; + } + + public void setRepoOwner(String repoOwner) { + this.repoOwner = repoOwner; + } + + public String getLogin() { + return login; + } + + public int getContributions() { + return contributions; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public String getRepoName() { + return repoName; + } + + public String getRepoOwner() { + return repoOwner; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java new file mode 100644 index 000000000..1673d10a6 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Repo.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.vo; + +import com.google.gson.annotations.SerializedName; + +import android.arch.persistence.room.Embedded; +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.Index; + +import javax.inject.Inject; + +/** + * Using name/owner_login as primary key instead of id since name/owner_login is always available + * vs id is not. + */ +@Entity(indices = {@Index("id"), @Index("owner_login")}, + primaryKeys = {"name", "owner_login"}) +public class Repo { + public static final int UNKNOWN_ID = -1; + public final int id; + @SerializedName("name") + public final String name; + @SerializedName("full_name") + public final String fullName; + @SerializedName("description") + public final String description; + @SerializedName("stargazers_count") + public final int stars; + @SerializedName("owner") + @Embedded(prefix = "owner_") + public final Owner owner; + + public Repo(int id, String name, String fullName, String description, Owner owner, int stars) { + this.id = id; + this.name = name; + this.fullName = fullName; + this.description = description; + this.owner = owner; + this.stars = stars; + } + + public static class Owner { + @SerializedName("login") + public final String login; + @SerializedName("url") + public final String url; + + public Owner(String login, String url) { + this.login = login; + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Owner owner = (Owner) o; + + if (login != null ? !login.equals(owner.login) : owner.login != null) { + return false; + } + return url != null ? url.equals(owner.url) : owner.url == null; + } + + @Override + public int hashCode() { + int result = login != null ? login.hashCode() : 0; + result = 31 * result + (url != null ? url.hashCode() : 0); + return result; + } + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java new file mode 100644 index 000000000..2b620df1b --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/RepoSearchResult.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.vo; + +import com.android.example.github.db.GithubTypeConverters; + +import android.arch.persistence.room.Entity; +import android.arch.persistence.room.TypeConverters; +import android.support.annotation.Nullable; + +import java.util.List; + +@Entity(primaryKeys = {"query"}) +@TypeConverters(GithubTypeConverters.class) +public class RepoSearchResult { + public final String query; + public final List repoIds; + public final int totalCount; + @Nullable + public final Integer next; + + public RepoSearchResult(String query, List repoIds, int totalCount, + Integer next) { + this.query = query; + this.repoIds = repoIds; + this.totalCount = totalCount; + this.next = next; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.java new file mode 100644 index 000000000..79327b705 --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.vo; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import static com.android.example.github.vo.Status.ERROR; +import static com.android.example.github.vo.Status.LOADING; +import static com.android.example.github.vo.Status.SUCCESS; + +/** + * A generic class that holds a value with its loading status. + * @param + */ +public class Resource { + + @NonNull + public final Status status; + + @Nullable + public final String message; + + @Nullable + public final T data; + + public Resource(@NonNull Status status, @Nullable T data, @Nullable String message) { + this.status = status; + this.data = data; + this.message = message; + } + + public static Resource success(@Nullable T data) { + return new Resource<>(SUCCESS, data, null); + } + + public static Resource error(String msg, @Nullable T data) { + return new Resource<>(ERROR, data, msg); + } + + public static Resource loading(@Nullable T data) { + return new Resource<>(LOADING, data, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Resource resource = (Resource) o; + + if (status != resource.status) { + return false; + } + if (message != null ? !message.equals(resource.message) : resource.message != null) { + return false; + } + return data != null ? data.equals(resource.data) : resource.data == null; + } + + @Override + public int hashCode() { + int result = status.hashCode(); + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (data != null ? data.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Resource{" + + "status=" + status + + ", message='" + message + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Status.java b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Status.java new file mode 100644 index 000000000..95a28952a --- /dev/null +++ b/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Status.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.example.github.vo; + +/** + * Status of a resource that is provided to the UI. + *