├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── auto-merge.yml
│ └── build.yml
├── .gitignore
├── BUILD
├── LICENSE
├── README.md
├── WORKSPACE
├── config
├── checkstyle-hard.xml
├── debug.keystore
├── fake.p12
└── lint.xml
├── proguard-rules.pro
├── src
├── androidTest
│ ├── AndroidManifest.xml
│ ├── BUILD
│ └── java
│ │ └── burrows
│ │ └── apps
│ │ └── example
│ │ └── template
│ │ └── instrumentation
│ │ └── MainActivityTest.java
├── main
│ ├── AndroidManifest.xml
│ ├── BUILD
│ ├── java
│ │ ├── AndroidManifest.xml
│ │ └── burrows
│ │ │ └── apps
│ │ │ └── example
│ │ │ └── template
│ │ │ ├── activity
│ │ │ └── MainActivity.java
│ │ │ ├── adapter
│ │ │ └── BaseAdapter.java
│ │ │ ├── fragment
│ │ │ └── PlaceholderFragment.java
│ │ │ └── util
│ │ │ ├── AdUtils.java
│ │ │ └── PlayServicesUtils.java
│ └── res
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ └── fragment_main.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ └── values
│ │ ├── strings.xml
│ │ └── styles.xml
└── test
│ ├── AndroidManifest.xml
│ ├── BUILD
│ └── java
│ └── burrows
│ └── apps
│ └── example
│ └── template
│ ├── activity
│ └── MainActivityTest.java
│ └── util
│ ├── AdUtilsTest.java
│ └── PlayServicesUtilsTest.java
└── versions.bzl
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # Change these settings to your own preference
4 | [*]
5 | indent_style = space
6 | indent_size = 4
7 |
8 | # We recommend you to keep these unchanged
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "daily"
8 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-merge
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - opened
7 | - synchronize
8 | check_suite:
9 | types:
10 | - completed
11 |
12 | permissions:
13 | contents: write
14 | pull-requests: write
15 |
16 | jobs:
17 | dependabot:
18 | runs-on: ubuntu-latest
19 | if: ${{ github.actor == 'dependabot[bot]' }} && ${{ github.event.check_suite.conclusion == 'success' }}
20 |
21 | steps:
22 | - name: Dependabot metadata
23 | id: metadata
24 | uses: dependabot/fetch-metadata@v1
25 | with:
26 | github-token: "${{ secrets.GITHUB_TOKEN }}"
27 |
28 | - name: Enable auto-merge for Dependabot PRs
29 | run: gh pr merge --auto --merge "$PR_URL"
30 | env:
31 | PR_URL: ${{ github.event.pull_request.html_url }}
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | types: [ opened, labeled, unlabeled, synchronize ]
9 |
10 | jobs:
11 | jvm:
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | api-level:
18 | - 23
19 | - 31
20 | java-version:
21 | - 11
22 | - 17
23 |
24 | steps:
25 | - name: Checkout Project
26 | uses: actions/checkout@v3.5.3
27 |
28 | - name: Cache Dependencies
29 | uses: actions/cache@v3
30 | with:
31 | path: "~/.cache/bazel"
32 | key: bazel
33 |
34 | - name: Configure JDK
35 | uses: actions/setup-java@v3.11.0
36 | with:
37 | distribution: temurin
38 | java-version: ${{ matrix.java-version }}
39 |
40 | - name: Run Build
41 | run: bazel build //... --disk_cache=$HOME/.cache/bazel_disk_cache --show_progress_rate_limit=5 --test_output=errors
42 |
43 | - name: Run Tests
44 | uses: reactivecircus/android-emulator-runner@v2.23.0
45 | if: matrix.api-level == '23'
46 | continue-on-error: true
47 | with:
48 | api-level: ${{ matrix.api-level }}
49 | script: bazel test //... --disk_cache=$HOME/.cache/bazel_disk_cache --show_progress_rate_limit=5 --test_output=errors
50 | env:
51 | API_LEVEL: ${{ matrix.api-level }}
52 |
53 | - name: Upload Artifacts
54 | uses: actions/upload-artifact@v2.3.1
55 | if: always()
56 | with:
57 | name: android-bazel-java-app-template-${{ github.workflow }}-${{ github.run_id }}
58 | path: |
59 | bazel-bin//src/main/template_app.apk
60 | if-no-files-found: warn
61 |
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### Android ###
4 | # Built application files
5 | *.apk
6 | *.ap_
7 |
8 | # Files for the Dalvik VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # Generated files
15 | bin/
16 | gen/
17 |
18 | # Gradle files
19 | .gradle/
20 | build/
21 | /*/build/
22 |
23 | # Local configuration file (sdk path, etc)
24 | local.properties
25 |
26 | # Proguard folder generated by Eclipse
27 | proguard/
28 |
29 | # Log Files
30 | *.log
31 |
32 | ### Android Patch ###
33 | gen-external-apklibs
34 |
35 |
36 | ### Intellij ###
37 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
38 |
39 | *.iml
40 |
41 | ## Directory-based project format:
42 | .idea/
43 | # if you remove the above rule, at least ignore the following:
44 |
45 | # User-specific stuff:
46 | # .idea/workspace.xml
47 | # .idea/tasks.xml
48 | # .idea/dictionaries
49 |
50 | # Sensitive or high-churn files:
51 | # .idea/dataSources.ids
52 | # .idea/dataSources.xml
53 | # .idea/sqlDataSources.xml
54 | # .idea/dynamic.xml
55 | # .idea/uiDesigner.xml
56 |
57 | # Gradle:
58 | # .idea/gradle.xml
59 | # .idea/libraries
60 |
61 | # Mongo Explorer plugin:
62 | # .idea/mongoSettings.xml
63 |
64 | ## File-based project format:
65 | *.ipr
66 | *.iws
67 |
68 | ## Plugin-specific files:
69 |
70 | # IntelliJ
71 | out/
72 |
73 | # mpeltonen/sbt-idea plugin
74 | .idea_modules/
75 |
76 | # JIRA plugin
77 | atlassian-ide-plugin.xml
78 |
79 | # Crashlytics plugin (for Android Studio and IntelliJ)
80 | com_crashlytics_export_strings.xml
81 | crashlytics.properties
82 | crashlytics-build.properties
83 |
84 |
85 | ### Java ###
86 | *.class
87 |
88 | # Mobile Tools for Java (J2ME)
89 | .mtj.tmp/
90 |
91 | # Package Files #
92 | *.jar
93 | *.war
94 | *.ear
95 |
96 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
97 | hs_err_pid*
98 |
99 |
100 | ### Gradle ###
101 | .gradle
102 | build/
103 |
104 | # Ignore Gradle GUI config
105 | gradle-app.setting
106 |
107 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
108 | !gradle-wrapper.jar
109 |
110 | .aswb/
111 | bazel-*
112 | .ijwb
113 | .DS_Store
114 |
--------------------------------------------------------------------------------
/BUILD:
--------------------------------------------------------------------------------
1 | exports_files(["versions.bzl"])
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Bazel Java App Template
2 |
3 | [](http://www.apache.org/licenses/LICENSE-2.0)
4 | [](https://github.com/jaredsburrows/android-bazel-java-app-template/actions/workflows/build.yml)
5 | [](https://twitter.com/jaredsburrows)
6 |
7 | Bazel + Android Studio + Robolectric + Espresso + Mockito + EasyMock/PowerMock
8 |
9 | ## Technologies used:
10 | #### Build Tools:
11 | | Name | Description |
12 | |------------------------------------------------------------------------------------------|----------------------|
13 | | [Bazel](https://bazel.build) | Bazel build system |
14 | | [Android SDK](http://developer.android.com/tools/revisions/platforms.html#5.1) | Official SDK |
15 | | [Android SDK Build Tools](http://developer.android.com/tools/revisions/build-tools.html) | Official Build Tools |
16 | | [Android Studio](http://tools.android.com/recent) or | Official IDE |
17 | | [Intellij](https://www.jetbrains.com/idea/download/) | Intellij IDE |
18 |
19 | #### Testing Frameworks:
20 | | Name | Description |
21 | |-----------------------------------------------------------------------|---------------------------|
22 | | [Espresso](https://google.github.io/android-testing-support-library/) | Instrumentation Framework |
23 | | [Robolectric](https://github.com/robolectric/robolectric) | Unit Testing Framework |
24 |
25 | # Getting Started:
26 | ## `Android Studio` or `Intellij` Support (Simple):
27 | - **Import/Open this project with Android Studio/Intellij**
28 | 1. Install the [Bazel plugin](https://ij.bazel.build/).
29 | 1. In the project selection dialog, click "Import Bazel Project".
30 | 1. For the project view, select "Create from scratch".
31 | 1. Click "Finish".
32 |
33 | ## Building and Running
34 |
35 | This project builds with [Bazel](https://bazel.build), Bazel's [Android
36 | rules](https://docs.bazel.build/versions/master/be/android.html), and the
37 | Android Build [tools](http://tools.android.com/tech-docs/new-build-system).
38 |
39 | **Build the APK:**
40 |
41 | $ bazel build //src/main:template_app
42 |
43 | **Install the APK:**
44 |
45 | $ bazel mobile-install //src/main:template_app
46 |
47 | or:
48 |
49 | $ bazel build //src/main:template_app && adb install bazel-bin/src/main/template_app.apk
50 |
51 | **Run the App:**
52 |
53 | $ bazel mobile-install //src/main:template_app --start_app
54 |
55 | > *Note:* Expect the first build to take a few minutes, depending on your
56 | > machine, because Bazel's cache is clean. After Bazel downloads and builds the
57 | > app once, subsequent builds will be much faster.
58 |
59 | ## Testing
60 |
61 | **Running the Unit Tests:**
62 |
63 | The [Junit](http://junit.org/junit4/) and
64 | [Robolectric](https://github.com/robolectric/robolectric) tests run on the JVM,
65 | no need for emulators or real devices.
66 |
67 | $ bazel test //src/test:all
68 |
69 | **Run a single unit test (`android_local_test`):**
70 |
71 | $ bazel test //src/test:play_services_utils_test
72 |
73 | **Get the list of all `android_local_test` targets:**
74 |
75 | $ bazel query 'kind(android_local_test, //src/test/...)'
76 |
77 | **Running the Instrumentation Tests:**
78 |
79 | The
80 | [Espresso](https://developer.android.com/training/testing/ui-testing/espresso-testing.html)
81 | instrumentation tests run on the device. There is no need to launch an emulator,
82 | Bazel will do it automatically as part of the instrumentation test.
83 |
84 | This is currently only supported on Linux.
85 |
86 | $ bazel test //src/androidTest:main_activity_test
87 |
88 | Read the [Bazel docs
89 | here](https://docs.bazel.build/versions/master/android-instrumentation-test.html).
90 |
91 |
--------------------------------------------------------------------------------
/WORKSPACE:
--------------------------------------------------------------------------------
1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
2 | load("@bazel_tools//tools/build_defs/repo:jvm.bzl", "jvm_maven_import_external")
3 | load("//:versions.bzl", "versions")
4 |
5 | android_sdk_repository(name = "androidsdk", build_tools_version = "30.0.3") # resolve dx error
6 |
7 | ATS_TAG = "1edfdab3134a7f01b37afabd3eebfd2c5bb05151"
8 | ATS_SHA256 = "dcd1ff76aef1a26329d77863972780c8fe1fc8ff625747342239f0489c2837ec"
9 | RULES_MAVEN_TAG = "0.0.5"
10 | RULES_MAVEN_SHA = "ee8b989efdcc886aa86290b7db6d4c05b339ab739d38f34091d93d22ab8f7c4c"
11 | RULES_JVM_EXTERNAL_TAG = "3.1"
12 | RULES_JVM_EXTERNAL_SHA = "e246373de2353f3d34d35814947aa8b7d0dd1a58c2f7a6c41cfeaff3007c2d14"
13 |
14 | http_archive(
15 | name = "build_bazel_rules_android",
16 | urls = ["https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"],
17 | sha256 = "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
18 | strip_prefix = "rules_android-0.1.1",
19 | )
20 |
21 | http_archive(
22 | name = "android_test_support",
23 | sha256 = ATS_SHA256,
24 | strip_prefix = "android-test-%s" % ATS_TAG,
25 | urls = ["https://github.com/android/android-test/archive/%s.tar.gz" % ATS_TAG],
26 | )
27 |
28 | load("@android_test_support//:repo.bzl", "android_test_repositories")
29 |
30 | android_test_repositories()
31 |
32 | http_archive(
33 | name = "robolectric",
34 | urls = ["https://github.com/robolectric/robolectric-bazel/archive/4.7.3.tar.gz"],
35 | strip_prefix = "robolectric-bazel-4.7.3",
36 | )
37 |
38 | load("@robolectric//bazel:robolectric.bzl", "robolectric_repositories")
39 |
40 | robolectric_repositories()
41 |
42 | http_archive(
43 | name = "rules_jvm_external",
44 | sha256 = RULES_JVM_EXTERNAL_SHA,
45 | strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
46 | url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
47 | )
48 |
49 | load("@rules_jvm_external//:defs.bzl", "maven_install")
50 |
51 | TEST_DEPS = [
52 | "org.robolectric:robolectric:" + versions["robolectric"],
53 | "org.assertj:assertj-core:" + versions["assertj"],
54 | "junit:junit:" + versions["junit"],
55 | "androidx.test:annotation:" + versions["androidx.test"]["annotation"],
56 | "androidx.test:core:" + versions["androidx.test"]["core"],
57 | "androidx.test:runner:" + versions["androidx.test"]["runner"],
58 | "androidx.test:rules:" + versions["androidx.test"]["rules"],
59 | "androidx.test.ext:junit:" + versions["androidx.test"]["ext"]["junit"],
60 | "androidx.test.espresso:espresso-core:" + versions["espresso"],
61 | "org.easymock:easymock:" + versions["easymock"],
62 | "org.powermock:powermock-core:" + versions["powermock"],
63 | "org.powermock:powermock-module-junit4:" + versions["powermock"],
64 | "org.powermock:powermock-api-easymock:" + versions["powermock"],
65 | ]
66 |
67 | maven_install(
68 | artifacts = [
69 | "com.google.android.material:material:" + versions["material"],
70 | "androidx.cardview:cardview:" + versions["cardview"],
71 | "com.google.android.gms:play-services-ads:" + versions["gps"],
72 | "com.google.android.gms:play-services-basement:" + versions["gps"],
73 | "com.google.android.gms:play-services-base:" + versions["gps"],
74 | "androidx.annotation:annotation:" + versions["annotation"],
75 | ] + TEST_DEPS,
76 | repositories = [
77 | "https://maven.google.com",
78 | "https://repo1.maven.org/maven2",
79 | ],
80 | )
81 |
82 |
--------------------------------------------------------------------------------
/config/checkstyle-hard.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
--------------------------------------------------------------------------------
/config/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsburrows/android-bazel-java-app-template/afd800ab1d91f50439e1b2f559defcc787f7d2c9/config/debug.keystore
--------------------------------------------------------------------------------
/config/fake.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsburrows/android-bazel-java-app-template/afd800ab1d91f50439e1b2f559defcc787f7d2c9/config/fake.p12
--------------------------------------------------------------------------------
/config/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # app
2 |
--------------------------------------------------------------------------------
/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/androidTest/BUILD:
--------------------------------------------------------------------------------
1 | load("@rules_jvm_external//:defs.bzl", "artifact")
2 | load("//:versions.bzl", "versions", "targetSdkVersion")
3 | load("@build_bazel_rules_android//android:rules.bzl", "android_binary", "android_instrumentation_test")
4 |
5 | android_instrumentation_test(
6 | name = "main_activity_test",
7 | tags = ["manual"], # Linux only, so this excludes it from //...
8 | target_device = "@android_test_support//tools/android/emulated_devices/generic_phone:android_23_x86_qemu2",
9 | test_app = ":template_test_app",
10 | )
11 |
12 | android_binary(
13 | name = "template_test_app",
14 | srcs = ["java/burrows/apps/example/template/instrumentation/MainActivityTest.java"],
15 | custom_package = "burrows.apps.example.template.instrumentation",
16 | instruments = "//src/main:template_app",
17 | manifest = "AndroidManifest.xml",
18 | manifest_values = {
19 | "targetSdkVersion": str(targetSdkVersion),
20 | },
21 | deps = [
22 | artifact("androidx.test:runner:" + versions["androidx.test"]["runner"]),
23 | artifact("androidx.test:rules:" + versions["androidx.test"]["runner"]),
24 | artifact("androidx.annotation:annotation:" + versions["annotation"]),
25 | artifact("androidx.test.espresso:espresso-core:" + versions["espresso"]),
26 | artifact("androidx.test.ext:junit:" + versions["androidx.test"]["ext"]["junit"]),
27 | artifact("junit:junit:" + versions["junit"]),
28 | "//src/main:template_app_lib",
29 | ],
30 | )
31 |
--------------------------------------------------------------------------------
/src/androidTest/java/burrows/apps/example/template/instrumentation/MainActivityTest.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.instrumentation;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.test.ext.junit.runners.AndroidJUnit4;
5 | import androidx.test.rule.ActivityTestRule;
6 | import burrows.apps.example.template.activity.MainActivity;
7 | import org.junit.Rule;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static androidx.test.espresso.Espresso.onView;
12 | import static androidx.test.espresso.assertion.ViewAssertions.matches;
13 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
14 | import static androidx.test.espresso.matcher.ViewMatchers.withText;
15 |
16 | @RunWith(AndroidJUnit4.class)
17 | public class MainActivityTest {
18 | @NonNull
19 | @Rule
20 | public ActivityTestRule activity = new ActivityTestRule<>(MainActivity.class);
21 |
22 | @Test
23 | public void shouldDisplayMainScreenWithCorrectTitle() {
24 | onView(withText("Android Bazel Template")).check(matches(isDisplayed()));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/main/BUILD:
--------------------------------------------------------------------------------
1 | load("@rules_jvm_external//:defs.bzl", "artifact")
2 | load("//:versions.bzl", "versions", "targetSdkVersion")
3 | load("@build_bazel_rules_android//android:rules.bzl", "android_binary", "android_library")
4 |
5 | android_binary(
6 | name = "template_app",
7 | custom_package = "burrows.apps.example.template",
8 | manifest = "AndroidManifest.xml",
9 | manifest_values = {
10 | "targetSdkVersion": str(targetSdkVersion),
11 | },
12 | visibility = ["//src/androidTest:__subpackages__"],
13 | deps = ["//src/main:template_app_lib"],
14 | )
15 |
16 | android_library(
17 | name = "template_app_lib",
18 | srcs = glob(["java/**/*.java"]),
19 | custom_package = "burrows.apps.example.template",
20 | manifest = "java/AndroidManifest.xml",
21 | resource_files = glob(["res/**/*"]),
22 | visibility = ["//src:__subpackages__"],
23 | deps = [":external_deps"],
24 | )
25 |
26 | android_library(
27 | name = "external_deps",
28 | exports = [
29 | artifact("com.google.android.material:material:" + versions["material"]),
30 | artifact("com.google.android.gms:play-services-ads:" + versions["gps"]),
31 | artifact("com.google.android.gms:play-services-ads-lite:" + versions["gps"]),
32 | artifact("com.google.android.gms:play-services-basement:" + versions["gps"]),
33 | artifact("com.google.android.gms:play-services-base:" + versions["gps"]),
34 | artifact("androidx.annotation:annotation:" + versions["annotation"]),
35 | artifact("androidx.cardview:cardview:" + versions["cardview"]),
36 | artifact("androidx.appcompat:appcompat:" + versions["material"]),
37 | artifact("androidx.recyclerview:recyclerview:" + versions["material"]),
38 | artifact("androidx.fragment:fragment:" + versions["material"]),
39 | ],
40 | )
41 |
--------------------------------------------------------------------------------
/src/main/java/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
21 |
30 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
50 |
51 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/main/java/burrows/apps/example/template/activity/MainActivity.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.activity;
2 |
3 | import android.os.Bundle;
4 | import androidx.annotation.Nullable;
5 | import androidx.appcompat.app.AppCompatActivity;
6 | import burrows.apps.example.template.R;
7 | import burrows.apps.example.template.fragment.PlaceholderFragment;
8 | import com.google.android.gms.ads.MobileAds;
9 |
10 | public class MainActivity extends AppCompatActivity {
11 | @Override
12 | protected void onCreate(@Nullable Bundle savedInstanceState) {
13 | super.onCreate(savedInstanceState);
14 | setContentView(R.layout.activity_main);
15 |
16 | MobileAds.initialize(this, getString(R.string.app_ads_id));
17 |
18 | if (savedInstanceState == null) {
19 | getSupportFragmentManager().beginTransaction()
20 | .add(R.id.container, new PlaceholderFragment(), PlaceholderFragment.class.getSimpleName())
21 | .commit();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/burrows/apps/example/template/adapter/BaseAdapter.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.adapter;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.recyclerview.widget.RecyclerView;
5 | import android.util.SparseBooleanArray;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Collections;
9 | import java.util.Comparator;
10 | import java.util.List;
11 |
12 | @SuppressWarnings("checkstyle:visibilitymodifier")
13 | public abstract class BaseAdapter extends RecyclerView.Adapter {
14 | /**
15 | * Data in the Adapter.
16 | */
17 | @NonNull
18 | protected List data = new ArrayList<>();
19 |
20 | /**
21 | * Selected items in the Adapter.
22 | */
23 | @NonNull
24 | protected SparseBooleanArray selectedItems = new SparseBooleanArray();
25 |
26 | /**
27 | * Returns the number of elements in the data.
28 | *
29 | * @return the number of elements in the data.
30 | */
31 | @Override
32 | public int getItemCount() {
33 | return data.size();
34 | }
35 |
36 | /**
37 | * Returns the instance of the data.
38 | *
39 | * @return instance of the data.
40 | */
41 | @NonNull
42 | public List getList() {
43 | return data;
44 | }
45 |
46 | /**
47 | * Returns the element at the specified location in the data.
48 | *
49 | * @param location the index of the element to return.
50 | * @return the element at the specified location.
51 | * @throws IndexOutOfBoundsException if {@code location < 0 || location >= size()}
52 | */
53 | @NonNull
54 | public T getItem(int location) {
55 | return data.get(location);
56 | }
57 |
58 | /**
59 | * Searches the data for the specified object and returns the index of the
60 | * first occurrence.
61 | *
62 | * @param object the object to search for.
63 | * @return the index of the first occurrence of the object or -1 if the
64 | * object was not found.
65 | */
66 | public int getLocation(@NonNull T object) {
67 | return data.indexOf(object);
68 | }
69 |
70 | /**
71 | * Clear the entire adapter using {@link RecyclerView.Adapter#notifyItemRangeRemoved}.
72 | */
73 | public void clear() {
74 | int size = data.size();
75 | if (size > 0) {
76 | for (int i = 0; i < size; i++) {
77 | data.remove(0);
78 | }
79 |
80 | notifyItemRangeRemoved(0, size);
81 | }
82 | }
83 |
84 | /**
85 | * Adds the specified object at the end of the data.
86 | *
87 | * @param object the object to add.
88 | * @return always true.
89 | * @throws UnsupportedOperationException if adding to the data is not supported.
90 | * @throws ClassCastException if the class of the object is inappropriate for this
91 | * data.
92 | * @throws IllegalArgumentException if the object cannot be added to the data.
93 | */
94 | public boolean add(@NonNull T object) {
95 | final boolean added = data.add(object);
96 | notifyItemInserted(data.size() + 1);
97 | return added;
98 | }
99 |
100 | /**
101 | * Adds the objects in the specified collection to the end of the data. The
102 | * objects are added in the order in which they are returned from the
103 | * collection's iterator.
104 | *
105 | * @param collection the collection of objects.
106 | * @return {@code true} if the data is modified, {@code false} otherwise
107 | * (i.e. if the passed collection was empty).
108 | * @throws UnsupportedOperationException if adding to the data is not supported.
109 | * @throws ClassCastException if the class of an object is inappropriate for this
110 | * data.
111 | * @throws IllegalArgumentException if an object cannot be added to the data.
112 | */
113 | public boolean addAll(@NonNull List collection) {
114 | final boolean added = data.addAll(collection);
115 | notifyItemRangeInserted(0, data.size() + 1);
116 | return added;
117 | }
118 |
119 | /**
120 | * Inserts the specified object into the data at the specified location.
121 | * The object is inserted before the current element at the specified
122 | * location. If the location is equal to the size of the data, the object
123 | * is added at the end. If the location is smaller than the size of the
124 | * data, then all elements beyond the specified location are moved by one
125 | * location towards the end of the data.
126 | *
127 | * @param location the index at which to insert.
128 | * @param object the object to add.
129 | * @throws UnsupportedOperationException if adding to the data is not supported.
130 | * @throws ClassCastException if the class of the object is inappropriate for this
131 | * data.
132 | * @throws IllegalArgumentException if the object cannot be added to the data.
133 | * @throws IndexOutOfBoundsException if {@code location < 0 || location > size()}
134 | */
135 | public void add(int location, @NonNull T object) {
136 | data.add(location, object);
137 | notifyItemInserted(location);
138 | }
139 |
140 | /**
141 | * Replaces the element at the specified location in the data with the
142 | * specified object. This operation does not change the size of the data.
143 | *
144 | * @param location the index at which to put the specified object.
145 | * @param object the object to insert.
146 | * @return the previous element at the index.
147 | * @throws UnsupportedOperationException if replacing elements in the data is not supported.
148 | * @throws ClassCastException if the class of an object is inappropriate for this
149 | * data.
150 | * @throws IllegalArgumentException if an object cannot be added to the data.
151 | * @throws IndexOutOfBoundsException if {@code location < 0 || location >= size()}
152 | */
153 | @NonNull
154 | public T set(int location, @NonNull T object) {
155 | final T insertedObject = data.set(location, object);
156 | notifyDataSetChanged();
157 | return insertedObject;
158 | }
159 |
160 | /**
161 | * Removes the first occurrence of the specified object from the data.
162 | *
163 | * @param object the object to remove.
164 | * @return true if the data was modified by this operation, false
165 | * otherwise.
166 | * @throws UnsupportedOperationException if removing from the data is not supported.
167 | */
168 | public boolean remove(int location, @NonNull T object) {
169 | final boolean removed = data.remove(object);
170 | notifyItemRangeRemoved(location, data.size());
171 | return removed;
172 | }
173 |
174 | /**
175 | * Removes the first occurrence of the specified object from the data.
176 | *
177 | * @param object the object to remove.
178 | * @return true if the data was modified by this operation, false
179 | * otherwise.
180 | * @throws UnsupportedOperationException if removing from the data is not supported.
181 | */
182 | public boolean remove(@NonNull T object) {
183 | final int location = getLocation(object);
184 | final boolean removed = data.remove(object);
185 | notifyItemRemoved(location);
186 | return removed;
187 | }
188 |
189 | /**
190 | * Removes the object at the specified location from the data.
191 | *
192 | * @param location the index of the object to remove.
193 | * @return the removed object.
194 | * @throws UnsupportedOperationException if removing from the data is not supported.
195 | * @throws IndexOutOfBoundsException if {@code location < 0 || location >= size()}
196 | */
197 | @NonNull
198 | public T remove(int location) {
199 | final T removedObject = data.remove(location);
200 | notifyItemRemoved(location);
201 | notifyItemRangeChanged(location, data.size());
202 | return removedObject;
203 | }
204 |
205 | /**
206 | * Sorts the given list using the given comparator. The algorithm is
207 | * stable which means equal elements don't get reordered.
208 | *
209 | * @throws ClassCastException if any element does not implement {@code Comparable},
210 | * or if {@code compareTo} throws for any pair of elements.
211 | */
212 | public void sort(@NonNull Comparator super T> comparator) {
213 | Collections.sort(data, comparator);
214 | notifyItemRangeChanged(0, getItemCount());
215 | }
216 |
217 | /**
218 | * Return the number of selected items.
219 | *
220 | * @return Number of selected items.
221 | */
222 | public int getItemSelectedCount() {
223 | return selectedItems.size();
224 | }
225 |
226 | /**
227 | * Return all selected IDs.
228 | *
229 | * @return Selected IDs.
230 | */
231 | @NonNull
232 | public SparseBooleanArray getSelectedItems() {
233 | return selectedItems;
234 | }
235 |
236 | /**
237 | * Return all selected items.
238 | *
239 | * @return Selected IDs.
240 | */
241 | @NonNull
242 | public List getSelectedList() {
243 | final List list = new ArrayList<>();
244 | final SparseBooleanArray selectedItems = getSelectedItems();
245 | for (int i = 0; i < selectedItems.size(); i++) {
246 | final T model = getList().get(selectedItems.keyAt(i));
247 | list.add(model);
248 | }
249 | return list;
250 | }
251 |
252 | /**
253 | * Remove all current selections.
254 | */
255 | public void removeSelections() {
256 | selectedItems.clear();
257 | notifyDataSetChanged();
258 | }
259 |
260 | /**
261 | * Toggle selection of item.
262 | *
263 | * @param location location of view.
264 | */
265 | public void toggleSelection(int location) {
266 | selectItem(location, !selectedItems.get(location));
267 | }
268 |
269 | /**
270 | * Change the current view state to selected.
271 | *
272 | * @param location location of view.
273 | * @param value True if view is selected.
274 | */
275 | public void selectItem(int location, boolean value) {
276 | if (value) {
277 | selectedItems.put(location, true);
278 | } else {
279 | selectedItems.delete(location);
280 | }
281 |
282 | notifyItemChanged(location);
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/src/main/java/burrows/apps/example/template/fragment/PlaceholderFragment.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.fragment;
2 |
3 | import android.os.Bundle;
4 | import androidx.annotation.NonNull;
5 | import androidx.annotation.Nullable;
6 | import androidx.fragment.app.Fragment;
7 | import androidx.appcompat.widget.AppCompatButton;
8 | import android.view.LayoutInflater;
9 | import android.view.View;
10 | import android.view.View.OnClickListener;
11 | import android.view.ViewGroup;
12 | import android.widget.Toast;
13 | import burrows.apps.example.template.R;
14 | import burrows.apps.example.template.util.AdUtils;
15 | import com.google.android.gms.ads.AdListener;
16 | import com.google.android.gms.ads.AdRequest;
17 | import com.google.android.gms.ads.AdView;
18 | import com.google.android.gms.ads.InterstitialAd;
19 |
20 | public class PlaceholderFragment extends Fragment {
21 | /**
22 | * Static Adview.
23 | */
24 | private AdView adView;
25 | /**
26 | * AdMob Full-Page Ad.
27 | */
28 | private InterstitialAd interstitialAd;
29 | /**
30 | * Button to launch Interstitial Ad.
31 | */
32 | private AppCompatButton startInterstitial;
33 | /**
34 | * ClickListener for Button.
35 | */
36 | private final OnClickListener onClickListener = v -> showInterstitialAd();
37 | private final AdListener adListener = new AdListener() {
38 | @Override
39 | public void onAdClosed() {
40 | super.onAdClosed();
41 | Toast.makeText(requireActivity(), "Ad has closed.", Toast.LENGTH_SHORT).show();
42 | }
43 |
44 | @Override
45 | public void onAdFailedToLoad(final int errorCode) {
46 | super.onAdFailedToLoad(errorCode);
47 | Toast.makeText(requireActivity(), "Ad has failed to load: " + AdUtils.getErrorReason(errorCode), Toast.LENGTH_SHORT).show();
48 | }
49 |
50 | @Override
51 | public void onAdLeftApplication() {
52 | super.onAdLeftApplication();
53 | Toast.makeText(requireActivity(), "Ad has left the application.", Toast.LENGTH_SHORT).show();
54 | }
55 |
56 | @Override
57 | public void onAdOpened() {
58 | super.onAdOpened();
59 | Toast.makeText(requireActivity(), "Ad has opened.", Toast.LENGTH_SHORT).show();
60 | }
61 |
62 | @Override
63 | public void onAdLoaded() {
64 | super.onAdLoaded();
65 | Toast.makeText(requireActivity(), "Ad has loaded.", Toast.LENGTH_SHORT).show();
66 | }
67 | };
68 |
69 | @NonNull
70 | @Override
71 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
72 | @Nullable Bundle savedInstanceState) {
73 | super.onCreateView(inflater, container, savedInstanceState);
74 |
75 | final View rootView = inflater.inflate(R.layout.fragment_main, container, false);
76 |
77 | startInterstitial = rootView.findViewById(R.id.buttonStartInterstitial);
78 | startInterstitial.setOnClickListener(onClickListener);
79 |
80 | adView = rootView.findViewById(R.id.adView);
81 | adView.setAdListener(adListener);
82 | adView.loadAd(new AdRequest.Builder().build());
83 |
84 | interstitialAd = new InterstitialAd(rootView.getContext());
85 | interstitialAd.setAdUnitId(getString(R.string.app_ad_interstitial));
86 | interstitialAd.setAdListener(adListener);
87 | interstitialAd.loadAd(new AdRequest.Builder().build());
88 |
89 | return rootView;
90 | }
91 |
92 | @Override
93 | public void onResume() {
94 | super.onResume();
95 |
96 | if (adView != null) {
97 | adView.resume();
98 | }
99 | }
100 |
101 | @Override
102 | public void onPause() {
103 | if (adView != null) {
104 | adView.pause();
105 | }
106 |
107 | super.onPause();
108 | }
109 |
110 | @Override
111 | public void onDestroy() {
112 | if (adView != null) {
113 | adView.destroy();
114 | }
115 |
116 | super.onDestroy();
117 | }
118 |
119 | private void showInterstitialAd() {
120 | if (interstitialAd.isLoaded()) {
121 | interstitialAd.show();
122 | } else {
123 | // Simply let the user know it has not been loaded and try again.
124 | Toast.makeText(requireActivity(), "Interstitial Ad has not loaded.", Toast.LENGTH_SHORT).show();
125 | interstitialAd.loadAd(new AdRequest.Builder().build());
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/java/burrows/apps/example/template/util/AdUtils.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.util;
2 |
3 | import androidx.annotation.IntRange;
4 | import androidx.annotation.NonNull;
5 | import com.google.android.gms.ads.AdRequest;
6 |
7 | public final class AdUtils {
8 | private AdUtils() {
9 | throw new AssertionError("No instances.");
10 | }
11 |
12 | @NonNull
13 | public static String getErrorReason(@IntRange(from = AdRequest.ERROR_CODE_INTERNAL_ERROR,
14 | to = AdRequest.ERROR_CODE_NO_FILL) int errorCode) {
15 | switch (errorCode) {
16 | default:
17 | case AdRequest.ERROR_CODE_INTERNAL_ERROR:
18 | return "Internal Error";
19 |
20 | case AdRequest.ERROR_CODE_INVALID_REQUEST:
21 | return "Invalid Request";
22 |
23 | case AdRequest.ERROR_CODE_NETWORK_ERROR:
24 | return "Network Error";
25 |
26 | case AdRequest.ERROR_CODE_NO_FILL:
27 | return "No Fill";
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/burrows/apps/example/template/util/PlayServicesUtils.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.util;
2 |
3 | import android.app.Activity;
4 | import android.app.Dialog;
5 | import android.content.DialogInterface;
6 | import androidx.annotation.NonNull;
7 | import com.google.android.gms.common.ConnectionResult;
8 | import com.google.android.gms.common.GoogleApiAvailability;
9 |
10 | /**
11 | * Google Play Services utility class.
12 | * https://developers.google.com/android/reference/com/google/android/gms/analytics/GoogleAnalytics. I have stopped
13 | * checking for Google Play Services because it pops up a dialog forcing a user to upgrade.
14 | */
15 | public final class PlayServicesUtils {
16 | private PlayServicesUtils() {
17 | throw new AssertionError("No instances.");
18 | }
19 |
20 | /**
21 | * Check if device has the correct Google Play Services version.
22 | *
23 | * @param activity Current activity.
24 | * @param availability New instance of GoogleApiAvailability.
25 | * @return True if there was a successful connection ot Google Play Services.
26 | */
27 | public static boolean hasGooglePlayServices(@NonNull Activity activity, @NonNull GoogleApiAvailability availability) {
28 | final int result = availability.isGooglePlayServicesAvailable(activity);
29 |
30 | if (result == ConnectionResult.SUCCESS) {
31 | return true;
32 | } else {
33 | final Dialog dialog = availability.getErrorDialog(activity, result, 0);
34 | // Let user use the application
35 | dialog.setOnCancelListener(DialogInterface::cancel);
36 | dialog.show();
37 | }
38 | return false;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/src/main/res/layout/fragment_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsburrows/android-bazel-java-app-template/afd800ab1d91f50439e1b2f559defcc787f7d2c9/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsburrows/android-bazel-java-app-template/afd800ab1d91f50439e1b2f559defcc787f7d2c9/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsburrows/android-bazel-java-app-template/afd800ab1d91f50439e1b2f559defcc787f7d2c9/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsburrows/android-bazel-java-app-template/afd800ab1d91f50439e1b2f559defcc787f7d2c9/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Android Bazel Template
5 | ca-app-pub-3940256099942544~3347511713
6 | ca-app-pub-3940256099942544/6300978111
7 | ca-app-pub-3940256099942544/1033173712
8 | Launch New Interstitial
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/test/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/test/BUILD:
--------------------------------------------------------------------------------
1 | load("@rules_jvm_external//:defs.bzl", "artifact")
2 | load("//:versions.bzl", "versions", "targetSdkVersion")
3 | load("@build_bazel_rules_android//android:rules.bzl", "android_local_test")
4 |
5 | android_local_test(
6 | name = "main_activity_test",
7 | srcs = ["java/burrows/apps/example/template/activity/MainActivityTest.java"],
8 | manifest = "AndroidManifest.xml",
9 | manifest_values = {
10 | "targetSdkVersion": str(targetSdkVersion)
11 | },
12 | test_class = "burrows.apps.example.template.activity.MainActivityTest",
13 | deps = [
14 | "@maven//:org_robolectric_robolectric",
15 | "@robolectric//bazel:android-all",
16 | "//src/main:template_app_lib",
17 | artifact("androidx.annotation:annotation:" + versions["annotation"]),
18 | artifact("androidx.test:annotation:" + versions["annotation"]),
19 | artifact("androidx.test:core:" + versions["androidx.test"]["core"]),
20 | artifact("androidx.test:runner:" + versions["androidx.test"]["runner"]),
21 | artifact("androidx.test:rules:" + versions["androidx.test"]["rules"]),
22 | artifact("androidx.test.ext:junit:" + versions["androidx.test"]["annotation"]),
23 | artifact("org.assertj:assertj-core:" + versions["assertj"]),
24 | artifact("org.robolectric.shadows:framework:" + versions["robolectric"]),
25 | ],
26 | )
27 |
28 | android_local_test(
29 | name = "ads_util_test",
30 | srcs = ["java/burrows/apps/example/template/util/AdUtilsTest.java"],
31 | manifest = "AndroidManifest.xml",
32 | manifest_values = {
33 | "targetSdkVersion": str(targetSdkVersion)
34 | },
35 | test_class = "burrows.apps.example.template.util.AdUtilsTest",
36 | deps = [
37 | "@maven//:org_robolectric_robolectric",
38 | "@robolectric//bazel:android-all",
39 | "//src/main:template_app_lib",
40 | artifact("org.hamcrest:hamcrest-library:1.3"),
41 | artifact("com.google.android.gms:play-services-ads-lite:" + versions["gps"]),
42 | ],
43 | )
44 |
45 | android_local_test(
46 | name = "play_services_utils_test",
47 | srcs = ["java/burrows/apps/example/template/util/PlayServicesUtilsTest.java"],
48 | manifest = "AndroidManifest.xml",
49 | manifest_values = {
50 | "targetSdkVersion": str(targetSdkVersion)
51 | },
52 | test_class = "burrows.apps.example.template.util.PlayServicesUtilsTest",
53 | deps = [
54 | "@maven//:org_robolectric_robolectric",
55 | "@robolectric//bazel:android-all",
56 | "//src/main:template_app_lib",
57 | artifact("androidx.annotation:annotation:" + versions["annotation"]),
58 | artifact("org.hamcrest:hamcrest-library:1.3"),
59 | artifact("org.easymock:easymock:" + versions["easymock"]),
60 | artifact("com.google.android.gms:play-services-basement:" + versions["gps"]),
61 | artifact("com.google.android.gms:play-services-base:" + versions["gps"]),
62 | artifact("org.powermock:powermock-api-easymock:" + versions["powermock"]),
63 | artifact("org.powermock:powermock-api-support:" + versions["powermock"]),
64 | artifact("org.powermock:powermock-core:" + versions["powermock"]),
65 | artifact("org.powermock:powermock-module-junit4:" + versions["powermock"]),
66 | ],
67 | )
68 |
--------------------------------------------------------------------------------
/src/test/java/burrows/apps/example/template/activity/MainActivityTest.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.activity;
2 |
3 | import android.app.Activity;
4 | import org.junit.Test;
5 | import org.junit.runner.RunWith;
6 | import org.robolectric.Robolectric;
7 | import org.robolectric.android.controller.ActivityController;
8 |
9 | import androidx.test.ext.junit.runners.AndroidJUnit4;
10 |
11 | import static org.assertj.core.api.Assertions.assertThat;
12 |
13 | /**
14 | * Junit Test using Robolectric with AssertJ matchers.
15 | */
16 | @RunWith(AndroidJUnit4.class)
17 | public class MainActivityTest {
18 | @Test
19 | public void testOnCreateNotNull() {
20 | ActivityController controller = Robolectric.buildActivity(MainActivity.class);
21 | Activity activity = controller.create().destroy().get();
22 |
23 | assertThat(activity).isNotNull();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/test/java/burrows/apps/example/template/util/AdUtilsTest.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.util;
2 |
3 | import org.junit.Test;
4 |
5 | import static burrows.apps.example.template.util.AdUtils.getErrorReason;
6 | import static com.google.android.gms.ads.AdRequest.ERROR_CODE_INTERNAL_ERROR;
7 | import static com.google.android.gms.ads.AdRequest.ERROR_CODE_INVALID_REQUEST;
8 | import static com.google.android.gms.ads.AdRequest.ERROR_CODE_NETWORK_ERROR;
9 | import static com.google.android.gms.ads.AdRequest.ERROR_CODE_NO_FILL;
10 | import static org.hamcrest.MatcherAssert.assertThat;
11 | import static org.hamcrest.Matchers.is;
12 |
13 | /**
14 | * Junit Test using Hamcrest matchers.
15 | */
16 | public class AdUtilsTest {
17 | @Test
18 | public void testGetErrorReason() {
19 | assertThat(getErrorReason(ERROR_CODE_INTERNAL_ERROR), is("Internal Error"));
20 | assertThat(getErrorReason(ERROR_CODE_INVALID_REQUEST), is("Invalid Request"));
21 | assertThat(getErrorReason(ERROR_CODE_NETWORK_ERROR), is("Network Error"));
22 | assertThat(getErrorReason(ERROR_CODE_NO_FILL), is("No Fill"));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/java/burrows/apps/example/template/util/PlayServicesUtilsTest.java:
--------------------------------------------------------------------------------
1 | package burrows.apps.example.template.util;
2 |
3 | import android.app.Activity;
4 | import android.app.Dialog;
5 | import android.content.DialogInterface;
6 | import androidx.annotation.IntRange;
7 | import com.google.android.gms.common.ConnectionResult;
8 | import com.google.android.gms.common.GoogleApiAvailability;
9 | import org.junit.Before;
10 | import org.junit.Test;
11 | import org.junit.runner.RunWith;
12 | import org.powermock.core.classloader.annotations.PrepareForTest;
13 | import org.powermock.modules.junit4.PowerMockRunner;
14 |
15 | import static burrows.apps.example.template.util.PlayServicesUtils.hasGooglePlayServices;
16 | import static com.google.android.gms.common.ConnectionResult.SUCCESS;
17 | import static org.easymock.EasyMock.anyObject;
18 | import static org.easymock.EasyMock.expect;
19 | import static org.hamcrest.MatcherAssert.assertThat;
20 | import static org.hamcrest.Matchers.is;
21 | import static org.powermock.api.easymock.PowerMock.createMock;
22 | import static org.powermock.api.easymock.PowerMock.expectLastCall;
23 | import static org.powermock.api.easymock.PowerMock.mockStatic;
24 | import static org.powermock.api.easymock.PowerMock.replay;
25 | import static org.powermock.api.easymock.PowerMock.replayAll;
26 | import static org.powermock.api.easymock.PowerMock.verifyAll;
27 | import static org.powermock.api.support.SuppressCode.suppressConstructor;
28 |
29 | /**
30 | * Junit Test using Powermock/Easymock with Hamcrest matchers.
31 | */
32 | @RunWith(PowerMockRunner.class)
33 | @PrepareForTest(GoogleApiAvailability.class)
34 | public class PlayServicesUtilsTest {
35 | private Activity mockActivity;
36 | private Dialog mockDialog;
37 | private GoogleApiAvailability mockSingleton;
38 |
39 | @Before
40 | public void setUp() {
41 | mockActivity = createMock(Activity.class);
42 | mockDialog = createMock(Dialog.class);
43 |
44 | suppressConstructor(GoogleApiAvailability.class);
45 | mockStatic(GoogleApiAvailability.class);
46 | mockSingleton = createMock(GoogleApiAvailability.class);
47 | expect(GoogleApiAvailability.getInstance()).andReturn(mockSingleton).anyTimes();
48 | }
49 |
50 | @Test
51 | public void testSuccess() {
52 | expect(mockSingleton.isGooglePlayServicesAvailable(mockActivity)).andReturn(SUCCESS);
53 |
54 | replay(GoogleApiAvailability.class);
55 | replay(mockSingleton);
56 | replayAll(mockActivity);
57 |
58 | assertThat(hasGooglePlayServices(mockActivity, GoogleApiAvailability.getInstance()), is(true));
59 | verifyAll();
60 | }
61 |
62 | @Test
63 | public void testServiceDisabled() {
64 | failTest(ConnectionResult.SERVICE_DISABLED);
65 | }
66 |
67 | @Test
68 | public void testServiceInvalid() {
69 | failTest(ConnectionResult.SERVICE_INVALID);
70 | }
71 |
72 | @Test
73 | public void testServiceMissing() {
74 | failTest(ConnectionResult.SERVICE_MISSING);
75 | }
76 |
77 | @Test
78 | public void testServiceVersionUpdateRequired() {
79 | failTest(ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED);
80 | }
81 |
82 | private void failTest(@IntRange(from = ConnectionResult.SUCCESS,
83 | to = ConnectionResult.RESTRICTED_PROFILE) int error) {
84 | expect(mockSingleton.isGooglePlayServicesAvailable(mockActivity)).andReturn(error);
85 | expect(mockSingleton.getErrorDialog(mockActivity, error, 0)).andReturn(mockDialog);
86 |
87 | mockDialog.setOnCancelListener(anyObject(DialogInterface.OnCancelListener.class));
88 | expectLastCall().once();
89 |
90 | mockDialog.show();
91 | expectLastCall().once();
92 |
93 | replayAll(mockActivity, mockDialog);
94 | assertThat(hasGooglePlayServices(mockActivity, GoogleApiAvailability.getInstance()), is(false));
95 | verifyAll();
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/versions.bzl:
--------------------------------------------------------------------------------
1 | versions = {
2 | "material": "1.5.0",
3 | "cardview": "1.0.0",
4 | "annotation": "1.3.0",
5 | "powermock": "2.0.9",
6 | "espresso": "3.4.0",
7 | "gps": "15.0.1",
8 | "easymock": "4.3",
9 | "junit": "4.13",
10 | "assertj": "1.7.1",
11 | "robolectric": "4.7.3",
12 | # Nested dictionaries
13 | "androidx.test": {
14 | "annotation": "1.0.0",
15 | "core": "1.4.0",
16 | "runner": "1.4.0",
17 | "rules": "1.4.0",
18 | "ext": {
19 | "junit": "1.1.3",
20 | },
21 | },
22 | }
23 |
24 | targetSdkVersion = 31
25 |
26 |
--------------------------------------------------------------------------------