├── .circleci └── config.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle ├── busybee-android ├── .gitignore ├── build.gradle.kts └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── americanexpress │ │ └── busybee │ │ └── internal │ │ ├── AndroidMainThreadExecutorTest.kt │ │ └── BusyBeeSingletonTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── io │ │ └── americanexpress │ │ └── busybee │ │ └── android │ │ └── internal │ │ ├── AndroidMainThreadExecutor.kt │ │ ├── BusyBeeIdlingResource.kt │ │ └── BusyBeeIdlingResourceRegistration.kt │ └── test │ └── java │ └── io │ └── americanexpress │ └── busybee │ └── internal │ ├── AndroidMainThreadExecutorJvmTest.kt │ └── BusyBeeSingletonJvmTest.kt ├── busybee-core ├── .gitignore ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── io │ │ └── americanexpress │ │ └── busybee │ │ ├── BusyBee.kt │ │ ├── BusyBeeExecutorWrapper.kt │ │ └── internal │ │ ├── BusyBeeSingleton.kt │ │ ├── EnvironmentChecks.kt │ │ ├── MainThread.kt │ │ ├── NoOpBusyBee.kt │ │ ├── RealBusyBee.kt │ │ ├── Reflection.kt │ │ └── SetMultiMap.kt │ └── test │ └── java │ └── io │ └── americanexpress │ └── busybee │ ├── BusyBeeTest.kt │ └── internal │ ├── BusyBeeExecutorWrapperTest.kt │ ├── BusyBeeSingletonTest.kt │ ├── EnvironmentChecksTest.kt │ ├── MainThreadTest.kt │ ├── NoOpBusyBeeTest.kt │ ├── ReflectionTest.kt │ └── SetMultiMapTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── busybee.png ├── sample-app ├── .gitignore ├── build.gradle.kts └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── americanexpress │ │ └── busybee │ │ ├── BusyBeeActivityTest.kt │ │ └── BusyBeeCategoryTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── americanexpress │ │ └── busybee │ │ └── sample │ │ └── BusyBeeActivity.kt │ └── res │ ├── layout │ └── activity_busy_bee.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | android-api29: 5 | docker: 6 | # https://hub.docker.com/layers/circleci/android/api-29/images/sha256-9fc0f34301182ba3730b1ca085e827927675168e57d412f6cc86256c7cd28e2d 7 | # api-29, JDK 11.0.10, commandlinetools-linux-6609375_latest.zip 8 | - image: circleci/android@sha256:9fc0f34301182ba3730b1ca085e827927675168e57d412f6cc86256c7cd28e2d 9 | 10 | 11 | anchors: 12 | - &common_job_config 13 | executor: android-api29 14 | 15 | 16 | jobs: 17 | test: 18 | <<: *common_job_config 19 | steps: 20 | # TODO: need to merge with destination branch before building 21 | - checkout 22 | - run: 23 | name: gradle test 24 | command: ./gradlew cleanTest test --stacktrace 25 | 26 | publish-snapshot: 27 | <<: *common_job_config 28 | steps: 29 | - checkout 30 | - run: 31 | name: Publish -SNAPSHOT 32 | command: MAVEN_REPO_URL="https://s01.oss.sonatype.org/content/repositories/snapshots/" ./gradlew build publish --stacktrace 33 | 34 | publish-release: 35 | <<: *common_job_config 36 | steps: 37 | - checkout 38 | - run: 39 | name: Publish Release 40 | command: MAVEN_REPO_URL="https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" ./gradlew build publish -Dsnapshot=false --stacktrace 41 | 42 | 43 | workflows_setup: 44 | - &context 45 | context: 46 | - maven_central_credentials 47 | - code_signing_credentials 48 | 49 | - &default_branch 50 | "main" 51 | 52 | workflows: 53 | pull_request_workflow: 54 | jobs: 55 | - test: 56 | filters: 57 | branches: 58 | ignore: *default_branch 59 | 60 | default_branch_workflow: 61 | when: 62 | # TODO: publish -SNAPSHOT for branches with branch name in the version 63 | equal: [ *default_branch, << pipeline.git.branch >> ] 64 | jobs: 65 | - test 66 | - publish-snapshot: 67 | <<: [ *context ] 68 | requires: 69 | - test 70 | 71 | 72 | publish_release: 73 | jobs: 74 | - publish-release: 75 | <<: [ *context ] 76 | filters: 77 | tags: 78 | only: /^\d+\.\d+\.\d+/ 79 | branches: 80 | ignore: /.*/ 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /.circleci/processed.yml 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ### American Express Open Source Community Guidelines 2 | 3 | #### Last Modified: January 29, 2016 4 | 5 | Welcome to the American Express Open Source Community on GitHub! These American Express Community 6 | Guidelines outline our expectations for Github participating members within the American Express 7 | community, as well as steps for reporting unacceptable behavior. We are committed to providing a 8 | welcoming and inspiring community for all and expect our community Guidelines to be honored. 9 | 10 | **IMPORTANT REMINDER:** 11 | 12 | When you visit American Express on any third party sites such as GitHub your activity there is 13 | subject to that site’s then current terms of use., along with their privacy and data security 14 | practices and policies. The Github platform is not affiliated with us and may have practices and 15 | policies that are different than are our own. 16 | 17 | Please note, American Express is not responsible for, and does not control, the GitHub site’s terms 18 | of use, privacy and data security practices and policies. You should, therefore, always exercise 19 | caution when posting, sharing or otherwise taking any action on that site and, of course, on the 20 | Internet in general. 21 | 22 | Our open source community strives to: 23 | - **Be friendly and patient**. 24 | - **Be welcoming**: We strive to be a community that welcomes and supports people of all 25 | backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, 26 | culture, national origin, color, immigration status, social and economic class, educational level, 27 | sex, sexual orientation, gender identity and expression, age, size, family status, political belief, 28 | religion, and mental and physical ability. 29 | - **Be considerate**: Your work will be used by other people, and you in turn will depend on the 30 | work of others. Any decision you take will affect users and colleagues, and you should take those 31 | consequences into account when making decisions. Remember that we're a world-wide community, so you 32 | might not be communicating in someone else's primary language. 33 | - **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor 34 | behavior and poor manners. We might all experience some frustration now and then, but we cannot 35 | allow that frustration to turn into a personal attack. It’s important to remember that a community 36 | where people feel uncomfortable or threatened is not a productive one. 37 | - **Be careful in the words that we choose**: We are a community of professionals, and we conduct 38 | ourselves professionally. Be kind to others. Do not insult or put down other participants. 39 | Harassment and other exclusionary behavior aren't acceptable. 40 | - **Try to understand why we disagree**: Disagreements, both social and technical, happen all the 41 | time. It is important that we resolve disagreements and differing views constructively. Remember 42 | that we’re all different people. The strength of our community comes from its diversity, people from 43 | a wide range of backgrounds. Different people have different perspectives on issues. Being unable to 44 | understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is 45 | human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve 46 | issues and learning from mistakes. 47 | 48 | ### Definitions 49 | Harassment includes, but is not limited to: 50 | - Offensive comments related to gender, gender identity and expression, sexual orientation, 51 | disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional 52 | discrimination, political or religious affiliation 53 | - Unwelcome comments regarding a person’s lifestyle choices and practices, including those related 54 | to food, health, parenting, drugs, and employment 55 | - Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not 56 | correctly reflect a person's gender identity. You must address people by the name they give you when 57 | not addressing them by their username or handle 58 | - Physical contact and simulated physical contact (eg, textual descriptions like “hug” or “backrub”) 59 | without consent or after a request to stop 60 | - Threats of violence, both physical and psychological 61 | - Incitement of violence towards any individual, including encouraging a person to commit suicide 62 | or to engage in self-harm 63 | - Deliberate intimidation 64 | - Stalking or following 65 | - Harassing photography or recording, including logging online activity for harassment purposes 66 | - Sustained disruption of discussion 67 | - Unwelcome sexual attention, including gratuitous or off-topic sexual images or behaviour 68 | - Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of 69 | intimacy with others 70 | - Continued one-on-one communication after requests to cease 71 | - Deliberate “outing” of any aspect of a person’s identity without their consent except as necessary 72 | to protect others from intentional abuse 73 | - Publication of non-harassing private communication 74 | 75 | Our open source community prioritizes marginalized people’s safety over privileged people’s comfort. 76 | We will not act on complaints regarding: 77 | - ‘Reverse’ -isms, including ‘reverse racism,’ ‘reverse sexism,’ and ‘cisphobia’ 78 | - Reasonable communication of boundaries, such as “leave me alone,” “go away,” or “I’m not 79 | discussing this with you” 80 | - Refusal to explain or debate social justice concepts 81 | - Communicating in a ‘tone’ you don’t find congenial 82 | - Criticizing racist, sexist, cissexist, or otherwise oppressive behavior or assumptions 83 | 84 | ### Diversity Statement 85 | We encourage everyone to participate and are committed to building a community for all. Although we 86 | will fail at times, we seek to treat everyone both as fairly and equally as possible. Whenever a 87 | participant has made a mistake, we expect them to take responsibility for it. If someone has been 88 | harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best 89 | to right the wrong. 90 | 91 | Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender 92 | identity or expression, culture, ethnicity, language, national origin, political beliefs, 93 | profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will 94 | not tolerate discrimination based on any of the protected characteristics above, including 95 | participants with disabilities. 96 | 97 | ### Reporting Issues 98 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by 99 | contacting us at opensource@aexp.com. All reports will be handled with discretion. In your report 100 | please include: 101 | - Your contact information. 102 | - Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional 103 | witnesses, please include them as well. Your account of what occurred, and if you believe the 104 | incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a 105 | public IRC logger), please include a link. 106 | - Any additional information that may be helpful. 107 | 108 | After filing a report, a representative of our community will contact you personally, review the 109 | incident, follow up with any additional questions, and make a decision as to how to respond. If the 110 | person who is harassing you is part of the response team, they will recuse themselves from handling 111 | your incident. If the complaint originates from a member of the response team, it will be handled by 112 | a different member of the response team. We will respect confidentiality requests for the purpose of 113 | protecting victims of abuse. 114 | 115 | ### Removal of Posts 116 | We will not review every comment or post, but we reserve the right to remove any that violates these 117 | Guidelines or that, in our sole discretion, we otherwise consider objectionable and we may ban 118 | offenders from our community. 119 | 120 | ### Suspension/Termination/Reporting to Authority 121 | In certain instances, we may suspend, terminate or ban certain repeat offenders and/or those 122 | committing significant violations of these Guidelines. When appropriate, we may also, on our own or 123 | as required by the GitHub terms of use, be required to refer and/or work with GitHub and/or the 124 | appropriate authorities to review and/or pursue certain violations. 125 | 126 | ### Attribution & Acknowledgements 127 | These Guidelines have been adapted from the 128 | [Code of Conduct of the TODO group](http://todogroup.org/opencodeofconduct/). They are subject to 129 | revision by American Express and may be revised from time to time. 130 | 131 | Thank you for your participation! 132 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 American Express Travel Related Services Company, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BusyBee - Tell Espresso when it needs to be patient because your app is busy 🐝 2 | 3 | 4 | 5 | BusyBee is an alternative API for [IdlingResource][]s in [Espresso][] tests. You can use BusyBee instead of 6 | [CountingIdlingResource][] to get better log messages and improve your ability to debug problems related to 7 | [IdlingResource][]s. 8 | 9 | BusyBee is meant to be used with [Espresso][]. You use BusyBee inside the "app under test". It allows the "app under 10 | test" to tell Espresso when it is `busyWith` an operation and, conversely, allows the app to tell Espresso when the 11 | operation is `completed`. Tracking `busyWith`/`completed` helps your **Espresso tests be fast and reliable**. 12 | 13 | If you write [Espresso][] tests, proper use of the [IdlingResource][] API is critical for ensuring that your tests are 14 | fast and reliable. IdlingResource can be hard to use correctly and it can be hard to understand what is happening with 15 | your IdlingResources when you are debugging problems with your tests. That is where BusyBee comes in. 16 | 17 | ## Comparison with [CountingIdlingResource][] 18 | 19 | In some ways, BusyBee is similar to [CountingIdlingResource][], but it does have some notable advantages: 20 | 21 | - Rather than track only the _number_ of operations in progress, BusyBee keeps track of the set of operations currently 22 | in progress. In progress operations are represented by a Java object, which could be a string, request object, etc. 23 | This allows for easier debugging, as it allows you to inspect the set of in progress operations across the whole app. 24 | - When Espresso times out because the app is busy, your logs can show the list of in progress operations. 25 | - BusyBee lets you separately enable/disable tracking of specific categories of operations (e.g. `NETWORK` operations) 26 | - The `BusyBee#completed(thing)` method is idempotent (`CountingIdlingResource#decrement` is not). This is useful when 27 | you have unreliable/multiple signals (e.g. WebView) to tell you that an operation has completed. Also, you can 28 | `completed(thing)` even if you never were `busyWith(thing)` 29 | 30 | **Trade-off:** While there are a number of advantages listed above, the downside of `BusyBee` (and 31 | `CountingIdlingResource`) is that you are modifying your app under test for purely testing purposes. 32 | 33 | # How to use BusyBee 34 | 35 | Include the BusyBee dependencies in your `build.gradle` files. When tests are not running, the **no-op** implementation 36 | is automatically used to minimize overhead of BusyBee (since it is only needed during tests). 37 | 38 | [Find the latest version on Maven Central](https://search.maven.org/artifact/io.americanexpress.busybee/busybee-core) 39 | 40 | _Required_: For Android modules: 41 | 42 | ```gradle 43 | implementation 'io.americanexpress.busybee:busybee-android:$version' 44 | ``` 45 | 46 | _Optional_: Only needed, if you want to use BusyBee in a non-Android module: 47 | 48 | ```gradle 49 | implementation 'io.americanexpress.busybee:busybee-core:$version' 50 | ``` 51 | 52 | BusyBee releases are available on Maven Central. 53 | 54 | ```gradle 55 | repositories { 56 | exclusiveContent { 57 | forRepository { mavenCentral() } 58 | filter { includeGroup("io.americanexpress.busybee") } 59 | } 60 | } 61 | ``` 62 | 63 | On each merge to `main`, a `-SNAPSHOT` version is published to https://s01.oss.sonatype.org/content/repositories/snapshots/ 64 | 65 | Inside your _app_, tell `BusyBee` what operations your app is `busyWith`, and when that operation is `completed`. 66 | 67 | ```java 68 | class BackgroundProcessor { 69 | private final BusyBee busyBee = BusyBee.singleton(); 70 | 71 | void processThing(Thing thing){ 72 | // Espresso will wait 73 | busyBee.busyWith(thing); 74 | try { 75 | thing.process(); 76 | } finally { 77 | // Espresso will continue 78 | busyBee.completed(thing); 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | That's all! Now Espresso will wait until your app is not busy before executing its actions and assertions. 85 | 86 | ## Categories 87 | 88 | Assigning a `Category` to your operations is an advanced feature of `BusyBee`. By default, all operations are in the 89 | `GENERAL` category. But, you can also add operations in other categories such as `NETWORK`. You can toggle tracking for 90 | any category with `payAttentionToCategory`/`ignoreCategory`. When a category is being "ignored" then Espresso will not 91 | wait for operations in that category. 92 | 93 | For example, you might want to perform actions on your UI or assert things about your UI while a network request is 94 | still in progress. In this case, you don't want Espresso to wait for the network requests to complete, but you still 95 | want Espresso to wait for other operations in your app. To accomplish this, you would use 96 | `busyBee.ignoreCategory(NETWORK)`, then perform actions and assertions on your UI, then call 97 | `busybee.payAttentionToCategory(NETWORK)` so Espresso will again wait for network operations to complete. 98 | 99 | ## BusyBeeExecutorWrapper 100 | 101 | If you have an executor and you need Espresso to know the app is "busy" anytime that executor is executing something, 102 | then you can wrap the `Executor` with `BusyBeeExecutorWrapper`. Operations executed with the wrapped `Executor` will 103 | cause `BusyBee` to be "busy" while they are in progress. 104 | 105 | ```java 106 | Executor backgroundTasks; 107 | Executor busyBeeBackgroundTasks = 108 | BusyBeeExecutorWrapper.with(busyBee) 109 | .wrapExecutor(backgroundTasks) 110 | .build(); 111 | busyBeeBackgroundTasks.execute(operation); 112 | ``` 113 | 114 | ## Contributing 115 | 116 | We welcome Your interest in the American Express Open Source Community on Github. Any Contributor to any Open Source 117 | Project managed by the American Express Open Source Community must accept and sign an Agreement indicating agreement to 118 | the terms below. Except for the rights granted in this Agreement to American Express and to recipients of software 119 | distributed by American Express, You reserve all right, title, and interest, if any, in and to Your Contributions. 120 | Please [fill out the Agreement][]. 121 | 122 | ## License 123 | 124 | Any contributions made under this project will be governed by the [Apache License 2.0][]. 125 | 126 | The Android™ robot is reproduced or modified from work created and shared by Google and used according to terms 127 | described in the Creative Commons 3.0 Attribution License. Android is a trademark of Google Inc. 128 | 129 | ## Code of Conduct 130 | 131 | This project adheres to the [American Express Community Guidelines][]. By participating, you are expected to honor these 132 | guidelines. 133 | 134 | [espresso]: https://developer.android.com/training/testing/espresso 135 | [idlingresource]: https://developer.android.com/reference/androidx/test/espresso/IdlingResource 136 | [countingidlingresource]: https://developer.android.com/reference/androidx/test/espresso/idling/CountingIdlingResource 137 | [fill out the agreement]: https://cla-assistant.io/americanexpress/busybee 138 | [apache license 2.0]: https://github.com/americanexpress/busybee/blob/main/LICENSE.txt 139 | [american express community guidelines]: https://github.com/americanexpress/busybee/blob/main/CODE_OF_CONDUCT.md 140 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | ## RELEASE INSTRUCTIONS 2 | 3 | For a manual release: 4 | 5 | 1. Make sure you have pulled the latest changes with `git pull` 6 | 2. Make sure you have no uncommitted changes with `git status` 7 | 3. Create a git tag with the version from `gradle.properties`. e.g. `git tag 1.0.1` 8 | 4. Push the tag. i.e. `git push --tags` 9 | 5. Build and publish the artifacts: `MAVEN_REPO_URL= ./gradlew publish -Dsnapshot=false` 10 | 6. Open a PR to increment `version` in `gradle.properties` to the next version 11 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import java.nio.charset.Charset 2 | 3 | /* 4 | * Copyright 2020 American Express Travel Related Services Company, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License 12 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 | * or implied. See the License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | buildscript { 17 | ext.kotlin_version = '1.6.0' 18 | repositories { 19 | mavenCentral() 20 | exclusiveContent { 21 | forRepository { google() } 22 | filter { 23 | includeGroupByRegex("androidx\\..*") 24 | includeGroupByRegex("com\\.android\\..*") 25 | includeGroup("com.android") 26 | includeGroup("com.google.testing.platform") 27 | includeGroup("com.google.android.material") 28 | } 29 | } 30 | } 31 | dependencies { 32 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.6.0' 33 | classpath 'com.android.tools.build:gradle:7.0.4' 34 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 35 | } 36 | } 37 | 38 | allprojects { 39 | repositories { 40 | mavenCentral() 41 | exclusiveContent { 42 | forRepository { google() } 43 | filter { 44 | includeGroupByRegex("androidx\\..*") 45 | includeGroupByRegex("com\\.android\\..*") 46 | includeGroup("com.android") 47 | includeGroup("com.google.testing.platform") 48 | includeGroup("com.google.android.material") 49 | } 50 | } 51 | } 52 | } 53 | 54 | def snapshot = System.getProperty("snapshot", "true").toBoolean() 55 | group = System.getProperty('group') 56 | version = System.getProperty('version') + (snapshot ? '-SNAPSHOT' : '') 57 | 58 | subprojects { 59 | ext.deps = [targetSdk : 29, 60 | compileSdk: 29, 61 | minSdk : 19, 62 | junit : '4.13.1', 63 | assertj : '2.6.0', 64 | mockito2 : '3.0.0', 65 | androidx : [ 66 | material : '1.0.0', 67 | annotation: '1.1.0', 68 | test : [ 69 | rules : '1.2.0', 70 | espresso: [ 71 | core : '3.2.0', 72 | idling_resource: '3.2.0' 73 | ] 74 | ] 75 | ] 76 | ] 77 | 78 | ext.compilerArgs = ["-Werror"] 79 | 80 | apply plugin: 'org.jetbrains.dokka' 81 | apply plugin: 'maven-publish' 82 | apply plugin: 'signing' 83 | 84 | group = rootProject.group 85 | version = rootProject.version 86 | 87 | afterEvaluate { 88 | if (plugins.hasPlugin('java-library')) { 89 | sourceCompatibility = 1.8 90 | targetCompatibility = 1.8 91 | } 92 | 93 | if (plugins.hasPlugin('org.jetbrains.kotlin.android')) { 94 | android.kotlinOptions { 95 | jvmTarget = "1.8" 96 | } 97 | } 98 | 99 | if (plugins.hasPlugin('org.jetbrains.kotlin.jvm')) { 100 | compileKotlin { 101 | kotlinOptions { 102 | jvmTarget = "1.8" 103 | } 104 | } 105 | 106 | compileTestKotlin { 107 | kotlinOptions { 108 | jvmTarget = "1.8" 109 | } 110 | } 111 | java { 112 | withSourcesJar() 113 | } 114 | } 115 | 116 | if (plugins.hasPlugin('com.android.library') 117 | || plugins.hasPlugin('com.android.application')) { 118 | android { 119 | 120 | lintOptions { 121 | disable "GradleDependency" 122 | } 123 | 124 | compileOptions { 125 | sourceCompatibility JavaVersion.VERSION_1_8 126 | targetCompatibility JavaVersion.VERSION_1_8 127 | } 128 | } 129 | } 130 | 131 | // Use getenv so they can be passed in without having them in a file 132 | // or in the command args 133 | def maven_username = System.getenv('MAVEN_CENTRAL_USERNAME') ?: '' 134 | def maven_password = System.getenv('MAVEN_CENTRAL_PASSWORD') ?: '' 135 | 136 | // from https://docs.gradle.org/current/userguide/publishing_maven.html 137 | if (plugins.hasPlugin('java-library')) { 138 | publishing { 139 | task dokkaJavadocJar(type: Jar) { 140 | from dokkaJavadoc.outputDirectory 141 | } 142 | repositories { 143 | maven { 144 | // Use getenv so they can be passed in without having them in a file 145 | // or in the command args 146 | credentials { 147 | username maven_username 148 | password maven_password 149 | } 150 | 151 | url System.getenv('MAVEN_REPO_URL') 152 | } 153 | } 154 | publications { 155 | // groupId and version come from gradle.properties 156 | // artifactId comes from the name of the gradle module 157 | busybee(MavenPublication) { 158 | from components.java 159 | artifact dokkaJavadocJar { 160 | getArchiveClassifier().set('javadoc') 161 | } 162 | pom { 163 | name = 'busybee' 164 | description = 'BusyBee is an alternative API for IdlingResources in Espresso tests.' 165 | url = 'https://github.com/americanexpress/busybee' 166 | licenses { 167 | license { 168 | name = 'The Apache License, Version 2.0' 169 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 170 | } 171 | } 172 | developers { 173 | developer { 174 | name = 'American Express Travel Related Services Company, Inc.' 175 | } 176 | } 177 | scm { 178 | connection = 'scm:git:https://github.com/americanexpress/busybee.git' 179 | developerConnection = 'scm:git:git@github.com:americanexpress/busybee.git' 180 | url = 'https://github.com/americanexpress/busybee' 181 | } 182 | } 183 | } 184 | } 185 | } 186 | def signingKeyBase64 = findProperty("signingKey_base64") 187 | if (signingKeyBase64 != null) { 188 | signing { 189 | def signingKey = new String(Base64.decoder.decode(signingKeyBase64), Charset.forName("UTF-8")) 190 | def signingPassword = findProperty("signingPassword") 191 | useInMemoryPgpKeys(signingKey, signingPassword) 192 | sign publishing.publications.busybee 193 | } 194 | } 195 | } 196 | 197 | if (plugins.hasPlugin('com.android.library')) { 198 | // need afterEvaluate else the components.release will be missing below 199 | afterEvaluate { 200 | // need this until it is built into AGP, see https://issuetracker.google.com/issues/145670440 201 | task androidSourcesJar(type: Jar) { 202 | from android.sourceSets.main.java.srcDirs 203 | } 204 | task dokkaJavadocJar(type: Jar) { 205 | from dokkaJavadoc.outputDirectory 206 | } 207 | publishing { 208 | repositories { 209 | maven { 210 | credentials { 211 | username maven_username 212 | password maven_password 213 | } 214 | 215 | url System.getenv('MAVEN_REPO_URL') 216 | } 217 | } 218 | publications { 219 | // groupId and version come from gradle.properties 220 | // artifactId comes from the name of the gradle module 221 | busybee(MavenPublication) { 222 | from components.release 223 | artifact androidSourcesJar { 224 | getArchiveClassifier().set('sources') 225 | } 226 | artifact dokkaJavadocJar { 227 | getArchiveClassifier().set('javadoc') 228 | } 229 | pom { 230 | name = 'busybee' 231 | description = 'BusyBee is an alternative API for IdlingResources in Espresso tests.' 232 | url = 'https://github.com/americanexpress/busybee' 233 | licenses { 234 | license { 235 | name = 'The Apache License, Version 2.0' 236 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 237 | } 238 | } 239 | developers { 240 | developer { 241 | name = 'American Express Travel Related Services Company, Inc.' 242 | } 243 | } 244 | scm { 245 | connection = 'scm:git:https://github.com/americanexpress/busybee.git' 246 | developerConnection = 'scm:git:git@github.com:americanexpress/busybee.git' 247 | url = 'https://github.com/americanexpress/busybee' 248 | } 249 | } 250 | } 251 | } 252 | def signingKeyBase64 = findProperty("signingKey_base64") 253 | if (signingKeyBase64 != null) { 254 | signing { 255 | def signingKey = new String(Base64.decoder.decode(signingKeyBase64), Charset.forName("UTF-8")) 256 | def signingPassword = findProperty("signingPassword") 257 | useInMemoryPgpKeys(signingKey, signingPassword) 258 | sign publishing.publications.busybee 259 | } 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /busybee-android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /busybee-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | plugins { 15 | id("com.android.library") 16 | id("org.jetbrains.kotlin.android") 17 | } 18 | 19 | android { 20 | compileSdkVersion = libs.versions.compileSdk.get() 21 | defaultConfig { 22 | minSdk = libs.versions.minimumSdk.get().toInt() 23 | targetSdk = libs.versions.targetSdk.get().toInt() 24 | version = 1 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | buildTypes { 28 | getByName("release") { 29 | isMinifyEnabled = false 30 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 31 | } 32 | } 33 | } 34 | 35 | 36 | dependencies { 37 | api(project(":busybee-core")) 38 | implementation(libs.kotlin.stdlib) 39 | implementation(libs.androidx.espresso.idling.resource) 40 | 41 | androidTestImplementation(libs.assertj.core) 42 | androidTestImplementation(libs.junit4) 43 | androidTestImplementation(libs.androidx.espresso.core) 44 | 45 | testImplementation(libs.assertj.core) 46 | testImplementation(libs.junit4) 47 | } 48 | -------------------------------------------------------------------------------- /busybee-android/src/androidTest/java/io/americanexpress/busybee/internal/AndroidMainThreadExecutorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor 18 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper 19 | import org.assertj.core.api.Java6Assertions.assertThat 20 | import org.junit.Test 21 | 22 | class AndroidMainThreadExecutorTest { 23 | @Test 24 | fun whenHasWorkingAndroidMainLooper_thenWeGetAndroidMainThreadExecutor() { 25 | assertThat(MainThread.singletonExecutor) 26 | .isInstanceOf(AndroidMainThreadExecutor::class.java) 27 | } 28 | 29 | @Test 30 | fun whenHasWorkingAndroidMainLooper_thenIsAndroidShouldBeTrue() { 31 | assertThat(hasWorkingAndroidMainLooper()).isTrue() 32 | } 33 | } -------------------------------------------------------------------------------- /busybee-android/src/androidTest/java/io/americanexpress/busybee/internal/BusyBeeSingletonTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent 18 | import io.americanexpress.busybee.internal.EnvironmentChecks.junit4IsPresent 19 | import org.assertj.core.api.Java6Assertions.assertThat 20 | import org.junit.Test 21 | 22 | class BusyBeeSingletonTest { 23 | 24 | @Test 25 | fun whenTestsAreRunning_thenWeDetectJunit4() { 26 | assertThat(junit4IsPresent()) 27 | .`as`("We are running tests, so this should be true") 28 | .isTrue() 29 | } 30 | 31 | @Test 32 | fun whenInAndroidTests_thenWeDetectItsAndroidRunner() { 33 | assertThat(androidJunitRunnerIsPresent()) 34 | .`as`("We are running /androidTest, so this should be true") 35 | .isTrue() 36 | } 37 | 38 | @Test 39 | fun whenTestsAreRunning_thenWeUseRealBusyBee() { 40 | assertThat(BusyBeeSingleton.singleton()) 41 | .`as`("We are running tests, so must use RealBusyBee") 42 | .isInstanceOf(RealBusyBee::class.java) 43 | } 44 | } -------------------------------------------------------------------------------- /busybee-android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /busybee-android/src/main/java/io/americanexpress/busybee/android/internal/AndroidMainThreadExecutor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.android.internal 15 | 16 | import android.os.Handler 17 | import android.os.Looper 18 | import java.util.concurrent.Executor 19 | 20 | enum class AndroidMainThreadExecutor : Executor { 21 | // accessed via reflection in busybee-core 22 | INSTANCE; 23 | 24 | private val handler = Handler(Looper.getMainLooper()) 25 | override fun execute(command: Runnable) { 26 | handler.post(command) 27 | } 28 | } -------------------------------------------------------------------------------- /busybee-android/src/main/java/io/americanexpress/busybee/android/internal/BusyBeeIdlingResource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.android.internal 15 | 16 | import androidx.test.espresso.IdlingResource 17 | import androidx.test.espresso.IdlingResource.ResourceCallback 18 | import io.americanexpress.busybee.BusyBee 19 | 20 | /** 21 | * This class is a bridge between espresso's IdlingResource and the app, so the app doesn't have to depend on espresso. 22 | * 23 | * 24 | * You must register it with Espresso: 25 | * `IdlingRegistry.getInstance().register(new BusyBeeIdlingResource(BusyBee.singleton()));` 26 | */ 27 | class BusyBeeIdlingResource internal constructor(private val busyBee: BusyBee) : IdlingResource { 28 | override fun getName() = busyBee.getName() 29 | 30 | override fun isIdleNow() = busyBee.isNotBusy() 31 | 32 | override fun registerIdleTransitionCallback(resourceCallback: ResourceCallback) { 33 | busyBee.registerNoLongerBusyCallback { resourceCallback.onTransitionToIdle() } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /busybee-android/src/main/java/io/americanexpress/busybee/android/internal/BusyBeeIdlingResourceRegistration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.android.internal 15 | 16 | import android.content.ContentProvider 17 | import android.content.ContentValues 18 | import android.database.Cursor 19 | import android.net.Uri 20 | import androidx.test.espresso.IdlingRegistry 21 | import io.americanexpress.busybee.BusyBee 22 | import io.americanexpress.busybee.internal.EnvironmentChecks 23 | 24 | /** 25 | * We register BusyBeeIdlingResource with espresso when the application loads. 26 | * 27 | * 28 | * Auto-registration inspired by LeakCanary 29 | * https://github.com/square/leakcanary/blob/934174edd0c04f2937733aae4a01f836c67b5b52/leakcanary-object-watcher-android/src/main/java/leakcanary/internal/AppWatcherInstaller.kt 30 | */ 31 | class BusyBeeIdlingResourceRegistration : ContentProvider() { 32 | override fun onCreate(): Boolean { 33 | if (EnvironmentChecks.testsAreRunning()) { 34 | IdlingRegistry.getInstance().register(BusyBeeIdlingResource(BusyBee.singleton())) 35 | } 36 | return true 37 | } 38 | 39 | override fun delete( 40 | uri: Uri, 41 | selection: String?, 42 | selectionArgs: Array? 43 | ) = 0 44 | 45 | override fun getType(uri: Uri): String? = null 46 | 47 | override fun insert(uri: Uri, values: ContentValues?): Uri? = null 48 | 49 | override fun query( 50 | uri: Uri, projection: Array?, selection: String?, 51 | selectionArgs: Array?, sortOrder: String? 52 | ): Cursor? = null 53 | 54 | override fun update( 55 | uri: Uri, values: ContentValues?, selection: String?, 56 | selectionArgs: Array? 57 | ) = 0 58 | } -------------------------------------------------------------------------------- /busybee-android/src/test/java/io/americanexpress/busybee/internal/AndroidMainThreadExecutorJvmTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor 18 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper 19 | import org.assertj.core.api.Java6Assertions.assertThat 20 | import org.junit.Test 21 | 22 | class AndroidMainThreadExecutorJvmTest { 23 | @Test 24 | fun whenDoesNotHaveWorkingAndroidMainLooper_thenWeDoNotGetAndroidMainThreadExecutor() { 25 | assertThat(MainThread.singletonExecutor) 26 | .isNotInstanceOf(AndroidMainThreadExecutor::class.java) 27 | } 28 | 29 | @Test 30 | fun whenDoesNotHaveWorkingAndroidMainLooper_thenHasWorkingAndroidMainLooperShouldBeFalse() { 31 | assertThat(hasWorkingAndroidMainLooper()).isFalse() 32 | } 33 | } -------------------------------------------------------------------------------- /busybee-android/src/test/java/io/americanexpress/busybee/internal/BusyBeeSingletonJvmTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent 18 | import io.americanexpress.busybee.internal.EnvironmentChecks.junit4IsPresent 19 | import org.assertj.core.api.Java6Assertions.assertThat 20 | import org.junit.Test 21 | 22 | class BusyBeeSingletonJvmTest { 23 | 24 | @Test 25 | fun whenJunit4IsUsed_thenWeDetectItsPresence() { 26 | assertThat(junit4IsPresent()) 27 | .`as`("We are running Junit4 tests, so this should be true") 28 | .isTrue() 29 | } 30 | 31 | @Test 32 | fun whenRunningJvmTestsWithoutEspresso_thenWeDontDetectIt() { 33 | assertThat(androidJunitRunnerIsPresent()) 34 | .`as`("We are in /test without espresso, so this should be false") 35 | .isFalse() 36 | } 37 | 38 | @Test 39 | fun whenTestsAreRunning_thenWeUseRealBusyBee() { 40 | assertThat(BusyBeeSingleton.singleton()) 41 | .`as`("We are running tests, so must use RealBusyBee") 42 | .isInstanceOf(RealBusyBee::class.java) 43 | } 44 | } -------------------------------------------------------------------------------- /busybee-core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /busybee-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | plugins { 15 | `java-library` 16 | id("org.jetbrains.kotlin.jvm") 17 | } 18 | 19 | dependencies { 20 | implementation(libs.androidx.annotation) 21 | // Android Studio 3.6 thinks we are using a different version of kotlin here, but we are not 22 | //noinspection DifferentStdlibGradleVersion 23 | implementation(libs.kotlin.stdlib) 24 | 25 | testImplementation(libs.assertj.core) 26 | testImplementation(libs.mockito.core) 27 | testImplementation(libs.junit4) 28 | } 29 | -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/BusyBee.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee 15 | 16 | import io.americanexpress.busybee.internal.BusyBeeSingleton 17 | 18 | interface BusyBee { 19 | fun getName(): String 20 | 21 | /** 22 | * Tell BusyBee that a new operation is now keeping the app "busy". 23 | * The operation that is passed in represents the ongoing operation. 24 | * Espresso will wait until `operation` is [.completed] (or an object that 25 | * is [Object.equals] to `operation` is completed. 26 | * 27 | * This operation will be placed in the [Category.defaultCategory]. 28 | * 29 | * @param operation An object that identifies the operation that is keeping the app "busy". 30 | * Must have a correct implementation of [Object.equals]/[Object.hashCode]. 31 | * Also, should have a meaningful [Object.toString]. 32 | */ 33 | fun busyWith(operation: Any) 34 | 35 | /** 36 | * Record the start of an async operation. 37 | * 38 | * @param operation An object that identifies the request. Must have a correct equals()/hashCode(). 39 | * @param category Which [Category] the given operation will be associated with 40 | */ 41 | fun busyWith(operation: Any, category: Category) 42 | 43 | /** 44 | * Get notified every time this BusyBee instance goes from being `busyWith` to no longer busy. 45 | * 46 | * @param noLongerBusyCallback callback 47 | */ 48 | fun registerNoLongerBusyCallback(noLongerBusyCallback: NoLongerBusyCallback) 49 | 50 | /** 51 | * If there are any operations in progress in the given category, then BusyBee will be in the busy state. 52 | * By default, BusyBee "pays attention" to all categories 53 | * 54 | * @param category - Category to stop ignoring 55 | * @see BusyBee.ignoreCategory 56 | */ 57 | fun payAttentionToCategory(category: Category) 58 | 59 | /** 60 | * If there are any operations in progress in the given category, then BusyBee will ignore those operations. 61 | * By default, BusyBee DOES NOT ignore any categories. 62 | * 63 | * 64 | * You can reverse this by calling [BusyBee.payAttentionToCategory] 65 | * 66 | * @param category - Category to be ignored 67 | * @see BusyBee.payAttentionToCategory 68 | */ 69 | fun ignoreCategory(category: Category) 70 | 71 | /** 72 | * All operations in progress in this category will be marked as completed (i.e. no longer busy) 73 | * 74 | * @param category Everything in this category will be completed. 75 | * @see BusyBee.busyWith 76 | */ 77 | fun completedEverythingInCategory(category: Category) 78 | 79 | /** 80 | * All operations in progress in all categories will be marked as completed (i.e. no longer busy) 81 | * 82 | * @see BusyBee.busyWith 83 | */ 84 | fun completedEverything() 85 | 86 | /** 87 | * `complete` all operations for which [OperationMatcher.matches] returns true 88 | * 89 | * @param matcher - Logic for selecting which operations will be completed 90 | * @see OperationMatcher 91 | */ 92 | fun completedEverythingMatching(matcher: OperationMatcher) 93 | 94 | /** 95 | * Marks an operation as complete, but the completion is done asynchronously (returns immediately) on the MainThread. 96 | * This means completion of the operation won't happen until this makes it to the front of the main thread queue. 97 | * 98 | * 99 | * The `operation` passed into this method should be something that was previously passed to [.busyWith] 100 | * If BusyBee wasn't already tracking the `operation` (either because it was already passed to completed() 101 | * or it was never [.busyWith] in the first place), then this method will have no effect. 102 | * 103 | * More info about the Android "main thread": https://www.youtube.com/watch?v=eAtMon8ndfk 104 | * 105 | * @param operation - Must have compliant implementations of `equals` and `hashcode` 106 | */ 107 | fun completed(operation: Any) 108 | 109 | /** 110 | * No operations that we are paying attention to is currently in progress 111 | * 112 | * @return true if and only if we are not busyWith an operation 113 | */ 114 | fun isNotBusy(): Boolean 115 | 116 | /** 117 | * @return true if and only if there are operations in progress (i.e. `busyWith`) for categories 118 | * we are "paying attention" to. 119 | * @see BusyBee.payAttentionToCategory 120 | * @see BusyBee.ignoreCategory 121 | */ 122 | fun isBusy(): Boolean 123 | 124 | /** 125 | * Dumps the state of this BusyBee instance, that can be used for debugging 126 | * 127 | * @return String with internal state of the BusyBee instance. 128 | */ 129 | fun toStringVerbose(): String 130 | fun interface NoLongerBusyCallback { 131 | /** 132 | * Called when there are no more operations in progress (that we are paying attention to) 133 | */ 134 | fun noLongerBusy() 135 | } 136 | 137 | fun interface OperationMatcher { 138 | fun matches(o: Any): Boolean 139 | } 140 | 141 | enum class Category { 142 | GENERAL, NETWORK, DIALOG; 143 | 144 | companion object { 145 | fun defaultCategory(): Category { 146 | return GENERAL 147 | } 148 | } 149 | } 150 | 151 | companion object { 152 | /** 153 | * Generally, you want just one instance of BusyBee for your whole process. 154 | * 155 | * 156 | * For release apps with no tests running, this will return a instance of BusyBee 157 | * that does nothing, so there is minimal overhead for your release builds. 158 | * 159 | * @return the single global instance of BusyBee 160 | */ 161 | fun singleton(): BusyBee { 162 | return BusyBeeSingleton.singleton() 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/BusyBeeExecutorWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee 15 | 16 | import io.americanexpress.busybee.BusyBee.Category.Companion.defaultCategory 17 | import io.americanexpress.busybee.internal.NoOpBusyBee 18 | import java.util.concurrent.Executor 19 | import java.util.logging.Logger 20 | 21 | /** 22 | * This is an implementation of the Executor interface that will track operations 23 | * using the BusyBee. I.e. when this executor is executing something, BusyBee 24 | * will be "busyWith" all operations in progress that are submitted to this executor. 25 | * 26 | * 27 | * All executed runnables will be execute using the wrapped executor. 28 | */ 29 | class BusyBeeExecutorWrapper private constructor( 30 | private val busyBee: BusyBee, 31 | private val category: BusyBee.Category, 32 | private val delegate: Executor 33 | ) : Executor { 34 | override fun execute(command: Runnable) { 35 | log.info("Starting $command on thread ${Thread.currentThread()}") 36 | busyBee.busyWith(command, category) 37 | delegate.execute { 38 | try { 39 | command.run() 40 | } finally { 41 | busyBee.completed(command) 42 | } 43 | } 44 | } 45 | 46 | class Builder { 47 | private lateinit var busyBee: BusyBee 48 | private var category: BusyBee.Category = defaultCategory() 49 | private var wrappedExecutor: Executor? = null 50 | fun busyBee(busyBee: BusyBee): Builder { 51 | this.busyBee = busyBee 52 | return this 53 | } 54 | 55 | fun executeInCategory(category: BusyBee.Category): Builder { 56 | this.category = category 57 | return this 58 | } 59 | 60 | fun wrapExecutor(delegate: Executor): Builder { 61 | wrappedExecutor = delegate 62 | return this 63 | } 64 | 65 | fun build(): Executor { 66 | val wrappedExecutorLocal = wrappedExecutor 67 | ?: throw NullPointerException("BusyBeeExecutorWrapper must have an underlying executor to wrap, can't be null.") 68 | return if (busyBee is NoOpBusyBee) { 69 | wrappedExecutorLocal 70 | } else { 71 | BusyBeeExecutorWrapper(busyBee, category, wrappedExecutorLocal) 72 | } 73 | } 74 | } 75 | 76 | companion object { 77 | private val log = Logger.getLogger("io.americanexpress.busybee") 78 | fun with(busyBee: BusyBee): Builder = Builder().busyBee(busyBee) 79 | } 80 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/internal/BusyBeeSingleton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import androidx.annotation.VisibleForTesting 17 | import io.americanexpress.busybee.BusyBee 18 | 19 | object BusyBeeSingleton { 20 | private val SINGLETON by lazy { create() } 21 | fun singleton(): BusyBee = SINGLETON 22 | 23 | @VisibleForTesting 24 | fun create(): BusyBee { 25 | return if (EnvironmentChecks.testsAreRunning()) { 26 | RealBusyBee(MainThread.singletonExecutor) 27 | } else { 28 | NoOpBusyBee() 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/internal/EnvironmentChecks.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import androidx.annotation.VisibleForTesting 17 | import io.americanexpress.busybee.internal.Reflection.classIsFound 18 | import io.americanexpress.busybee.internal.Reflection.clazz 19 | import io.americanexpress.busybee.internal.Reflection.invokeConstructor 20 | import io.americanexpress.busybee.internal.Reflection.invokeMethod 21 | import io.americanexpress.busybee.internal.Reflection.invokeStaticMethod 22 | import java.util.concurrent.ExecutionException 23 | import java.util.concurrent.FutureTask 24 | import java.util.concurrent.TimeUnit 25 | import java.util.concurrent.TimeoutException 26 | 27 | object EnvironmentChecks { 28 | @VisibleForTesting 29 | private var pretendTestsAreNotRunning = false 30 | fun testsAreRunning(): Boolean { 31 | return if (pretendTestsAreNotRunning) false else junit4IsPresent() || androidJunitRunnerIsPresent() 32 | 33 | // may want to add TestNG or Junit5 support at some point. 34 | } 35 | 36 | @VisibleForTesting 37 | fun junit4IsPresent(): Boolean = classIsFound("org.junit.runners.JUnit4") 38 | 39 | @VisibleForTesting 40 | fun androidJunitRunnerIsPresent(): Boolean = classIsFound("androidx.test.runner.AndroidJUnitRunner") 41 | 42 | fun hasWorkingAndroidMainLooper(): Boolean { 43 | val runnable: FutureTask 44 | try { 45 | val looperClass: Class<*> = clazz("android.os.Looper") 46 | 47 | val mainLooper = invokeStaticMethod(looperClass, "getMainLooper") 48 | val myLooper = invokeStaticMethod(looperClass, "myLooper") 49 | if (mainLooper == myLooper) { 50 | // we are already on the main looper so it must be working. 51 | return true 52 | } 53 | // else we will try and execute something on the main thread. 54 | val handlerClass: Class<*> = clazz("android.os.Handler") 55 | val handler = invokeConstructor(handlerClass, looperClass, mainLooper) 56 | runnable = FutureTask { true } 57 | invokeMethod( 58 | handler, 59 | "postAtFrontOfQueue", 60 | arrayOf(Runnable::class.java), 61 | arrayOf(runnable) 62 | ) 63 | } catch (e: RuntimeException) { 64 | if (e.cause is ReflectiveOperationException) { 65 | // something that we needed doesn't exist, so Android Main Looper won't work 66 | return false 67 | } 68 | throw e 69 | } 70 | return try { 71 | runnable.get(5, TimeUnit.SECONDS) 72 | } catch (e: InterruptedException) { 73 | false 74 | } catch (e: ExecutionException) { 75 | false 76 | } catch (e: TimeoutException) { 77 | false 78 | } 79 | } 80 | 81 | @VisibleForTesting 82 | fun pretendTestsAreNotRunning() { 83 | pretendTestsAreNotRunning = true 84 | } 85 | 86 | @VisibleForTesting 87 | fun doNotPretendTestsAreNotRunning() { 88 | pretendTestsAreNotRunning = false 89 | } 90 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/internal/MainThread.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper 17 | import io.americanexpress.busybee.internal.Reflection.clazz 18 | import io.americanexpress.busybee.internal.Reflection.getField 19 | import io.americanexpress.busybee.internal.Reflection.getValue 20 | import java.util.concurrent.Executor 21 | import java.util.concurrent.Executors.newSingleThreadExecutor 22 | 23 | object MainThread { 24 | val singletonExecutor: Executor by lazy { 25 | /* 26 | * Can only load AndroidMainThreadExecutor if we are on Android (not JVM) 27 | * else will we get class not found errors. 28 | */ 29 | return@lazy if (hasWorkingAndroidMainLooper()) { 30 | val androidExecutorClass = clazz( 31 | className = "io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor", 32 | notFoundErrorMessage = "Must add busybee-android dependency when running on Android" 33 | ) 34 | val instance = getField(androidExecutorClass, "INSTANCE") 35 | getValue(instance) as Executor 36 | } else { 37 | // use this on JVM when there is no Android Main Thread 38 | newSingleThreadExecutor() 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/internal/NoOpBusyBee.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import io.americanexpress.busybee.BusyBee 17 | import io.americanexpress.busybee.BusyBee.NoLongerBusyCallback 18 | import io.americanexpress.busybee.BusyBee.OperationMatcher 19 | 20 | /** 21 | * This is a version of BusyBee that is used when the tests are not running. 22 | * It does nothing. 23 | * This minimizes the overhead of having BusyBee in your app. 24 | */ 25 | class NoOpBusyBee internal constructor() : BusyBee { 26 | override fun getName(): String = "NO-OP BusyBee" 27 | 28 | override fun busyWith(operation: Any) {} 29 | override fun busyWith(operation: Any, category: BusyBee.Category) {} 30 | override fun registerNoLongerBusyCallback(noLongerBusyCallback: NoLongerBusyCallback) {} 31 | override fun payAttentionToCategory(category: BusyBee.Category) {} 32 | override fun ignoreCategory(category: BusyBee.Category) {} 33 | override fun completedEverythingInCategory(category: BusyBee.Category) {} 34 | override fun completedEverything() {} 35 | override fun completedEverythingMatching(matcher: OperationMatcher) {} 36 | override fun completed(operation: Any) {} 37 | override fun isNotBusy(): Boolean = true 38 | override fun isBusy(): Boolean = false 39 | 40 | override fun toStringVerbose(): String = "NO-OP BusyBee" 41 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/internal/RealBusyBee.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import androidx.annotation.GuardedBy 17 | import io.americanexpress.busybee.BusyBee 18 | import io.americanexpress.busybee.BusyBee.NoLongerBusyCallback 19 | import io.americanexpress.busybee.BusyBee.OperationMatcher 20 | import java.util.ArrayList 21 | import java.util.EnumSet 22 | import java.util.concurrent.Executor 23 | import java.util.concurrent.locks.Lock 24 | import java.util.concurrent.locks.ReentrantLock 25 | import java.util.logging.Logger 26 | import kotlin.concurrent.withLock 27 | 28 | /** 29 | * This allows the app to let Espresso (test framework) know when it is busy and Espresso should wait. 30 | * It is not directly tied to Espresso and could be used in any case where you need to know if the app is "busy". 31 | * 32 | * 33 | * Call busyWith when you start being "busy" 34 | * Call completed when you stop being "busy" and start being "idle". 35 | * 36 | * 37 | * Generally, you should call completed from a finally block. 38 | * 39 | * 40 | * Espresso will wait for the app to be "idle" (i.e. not busy). 41 | * 42 | * 43 | * Proper use of the BusyBee will avoid having to "wait" or "sleep" in tests. 44 | * BE SURE NOT BE "BUSY" LONGER THAN NECESSARY, otherwise it will slow down your tests. 45 | */ 46 | class RealBusyBee(private val completedOnThread: Executor) : BusyBee { 47 | @GuardedBy("lock") 48 | private val operationsInProgress = SetMultiMap() 49 | 50 | @GuardedBy("lock") 51 | private val currentlyTrackedCategories = EnumSet.allOf(BusyBee.Category::class.java) 52 | 53 | @GuardedBy("lock") 54 | private val noLongerBusyCallbacks: MutableList = 55 | ArrayList(1) // Espresso use case will only have 1 callback 56 | private val lock: Lock = ReentrantLock() 57 | private val defaultCategory: BusyBee.Category = BusyBee.Category.defaultCategory() 58 | override fun getName(): String = lock.withLock { 59 | "${this.javaClass.simpleName}@${System.identityHashCode(this)} with operations: $operationsInProgress" 60 | } 61 | 62 | override fun busyWith(operation: Any) { 63 | busyWith(operation, defaultCategory) 64 | } 65 | 66 | override fun busyWith(operation: Any, category: BusyBee.Category) { 67 | lock.withLock { 68 | if (operationsInProgress.add(category, operation)) { 69 | log.info("busyWith -> [$operation] was added to active operations in category $category") 70 | } 71 | } 72 | } 73 | 74 | override fun registerNoLongerBusyCallback(noLongerBusyCallback: NoLongerBusyCallback) { 75 | lock.withLock { 76 | noLongerBusyCallbacks.add(noLongerBusyCallback) 77 | } 78 | } 79 | 80 | override fun payAttentionToCategory(category: BusyBee.Category) { 81 | lock.withLock { 82 | log.info("Paying attention to category: $category") 83 | currentlyTrackedCategories.add(category) 84 | } 85 | } 86 | 87 | override fun ignoreCategory(category: BusyBee.Category) { 88 | lock.withLock { 89 | log.info("Ignoring category: $category") 90 | val wasBusyBefore = isBusy() 91 | val wasRemoved = currentlyTrackedCategories.remove(category) 92 | val notBusyNow = isNotBusy() 93 | if (wasRemoved && wasBusyBefore && notBusyNow) { 94 | notifyNoLongerBusyCallbacks() 95 | } 96 | } 97 | } 98 | 99 | override fun completedEverythingInCategory(category: BusyBee.Category) { 100 | completedOnThread.execute(object : Runnable { 101 | override fun run() { 102 | lock.withLock { 103 | val iterator = operationsInProgress.valuesIterator(category) 104 | while (iterator.hasNext()) { 105 | val next = iterator.next() 106 | completeOnCurrentThread(next, iterator) 107 | } 108 | } 109 | } 110 | 111 | override fun toString() = "completedEverythingInCategory($category)" 112 | }) 113 | } 114 | 115 | override fun completedEverything() { 116 | completedOnThread.execute(object : Runnable { 117 | override fun run() { 118 | lock.withLock { 119 | val iterator = operationsInProgress.valuesIterator() 120 | while (iterator.hasNext()) { 121 | val next = iterator.next() 122 | completeOnCurrentThread(next, iterator) 123 | } 124 | } 125 | } 126 | 127 | override fun toString() = "completedEverything()" 128 | }) 129 | } 130 | 131 | override fun completedEverythingMatching(matcher: OperationMatcher) { 132 | completedOnThread.execute(object : Runnable { 133 | override fun run() { 134 | lock.withLock { 135 | val iterator = operationsInProgress.valuesIterator() 136 | while (iterator.hasNext()) { 137 | val next = iterator.next() 138 | if (matcher.matches(next)) { 139 | completeOnCurrentThread(next, iterator) 140 | } 141 | } 142 | } 143 | } 144 | 145 | override fun toString() = "completedEverythingMatching($matcher)" 146 | }) 147 | } 148 | 149 | override fun completed(operation: Any) { 150 | completedOnThread.execute(object : Runnable { 151 | override fun run() { 152 | completeOnCurrentThread(operation, null) 153 | } 154 | 155 | override fun toString() = "completed($operation)" 156 | }) 157 | } 158 | 159 | /** 160 | * Precondition: Iterator must be pointing to the operation passed in (or iterator must be null). 161 | * 162 | * 163 | * This method "completes" the operation on the current thread. 164 | * 165 | * @param operation the operation to be completed 166 | * @param iterator must be pointing to operation 167 | */ 168 | private fun completeOnCurrentThread(operation: Any, iterator: MutableIterator?) { 169 | lock.withLock { 170 | val wasRemoved = if (iterator != null) { 171 | // if the collection is being iterated, 172 | // then we HAVE to use the iterator for removal to avoid ConcurrentModificationException 173 | iterator.remove() 174 | true 175 | } else { 176 | operationsInProgress.removeValue(operation) 177 | } 178 | if (wasRemoved) { 179 | log.info("completed -> [$operation] was removed from active operations") 180 | } 181 | if (wasRemoved && isNotBusy()) { 182 | notifyNoLongerBusyCallbacks() 183 | } 184 | } 185 | } 186 | 187 | private fun notifyNoLongerBusyCallbacks() { 188 | for (noLongerBusyCallback in noLongerBusyCallbacks) { 189 | log.info("All operations are now finished, we are now idle") 190 | noLongerBusyCallback.noLongerBusy() 191 | } 192 | } 193 | 194 | override fun isNotBusy(): Boolean { 195 | lock.withLock { 196 | for (category in currentlyTrackedCategories) { 197 | if (isBusyWithAnythingIn(category)) return false 198 | } 199 | return true 200 | } 201 | } 202 | 203 | override fun isBusy(): Boolean = !isNotBusy() 204 | 205 | private fun isBusyWithAnythingIn(category: BusyBee.Category) = lock.withLock { 206 | operationsInProgress.values(category).isNotEmpty() 207 | } 208 | 209 | override fun toStringVerbose(): String = lock.withLock { 210 | val operations = operationsInProgress 211 | buildString { 212 | appendLine( 213 | """ 214 | *********************** 215 | **BusyBee Information** 216 | *********************** 217 | """.trimIndent() 218 | ) 219 | try { 220 | appendLine( 221 | """ 222 | Total Operations: ${operations.allValues().size} 223 | List of operations in progress: 224 | **************************** 225 | """.trimIndent() 226 | ) 227 | for (category in operations.allKeys()) { 228 | appendLine( 229 | """ 230 | CATEGORY: ======= ${category.name} ======= 231 | """.trimIndent() 232 | ) 233 | for (operation in operations.values(category)) { 234 | appendLine(operation) 235 | } 236 | } 237 | } catch (e: Exception) { 238 | appendLine( 239 | """ 240 | ${e.message} 241 | ****!!!!FAILED TO GET LIST OF IN PROGRESS OPERATIONS!!!!**** 242 | """.trimIndent() 243 | ) 244 | } 245 | appendLine("****************************") 246 | } 247 | } 248 | 249 | companion object { 250 | private val log = Logger.getLogger("io.americanexpress.busybee") 251 | } 252 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/internal/Reflection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import java.lang.reflect.Field 17 | import java.lang.reflect.InvocationTargetException 18 | 19 | // we are doing a number of type-unsafe things here to seamlessly detect whether we are on Android or not. 20 | // Can't use the actual Android types because they might not exist, if we are in a pure JVM module 21 | internal object Reflection { 22 | fun getValue(instance: Field): Any = try { 23 | instance.get(null) 24 | } catch (e: IllegalAccessException) { 25 | throw RuntimeException(e) 26 | } 27 | 28 | fun getField(clazz: Class<*>, fieldName: String): Field = try { 29 | clazz.getDeclaredField(fieldName) 30 | } catch (e: NoSuchFieldException) { 31 | throw RuntimeException(e) 32 | } 33 | 34 | fun invokeStaticMethod(clazz: Class<*>, methodName: String): Any = try { 35 | clazz.getMethod(methodName).invoke(null) 36 | } catch (e: NoSuchMethodException) { 37 | throw RuntimeException(e) 38 | } catch (e: IllegalAccessException) { 39 | throw RuntimeException(e) 40 | } catch (e: InvocationTargetException) { 41 | throw RuntimeException(e) 42 | } 43 | 44 | fun invokeMethod( 45 | instance: Any, 46 | methodName: String, 47 | argTypes: Array>, 48 | args: Array, 49 | ): Any = try { 50 | instance.javaClass.getMethod(methodName, *argTypes).invoke(instance, *args) 51 | } catch (e: NoSuchMethodException) { 52 | throw RuntimeException(e) 53 | } catch (e: IllegalAccessException) { 54 | throw RuntimeException(e) 55 | } catch (e: InvocationTargetException) { 56 | throw RuntimeException(e) 57 | } 58 | 59 | @JvmOverloads 60 | fun clazz( 61 | className: String, 62 | notFoundErrorMessage: String = "Error calling Class.forName on $className", 63 | ): Class<*> = try { 64 | Class.forName(className) 65 | } catch (e: ClassNotFoundException) { 66 | throw RuntimeException(notFoundErrorMessage, e) 67 | } 68 | 69 | fun classIsFound(className: String): Boolean { 70 | try { 71 | Class.forName(className) 72 | } catch (e: ClassNotFoundException) { 73 | return false 74 | } 75 | return true 76 | } 77 | 78 | fun invokeConstructor( 79 | classToConstruct: Class<*>, 80 | constructorArgType: Class<*>, 81 | constructorArg: Any, 82 | ): Any = try { 83 | classToConstruct.getConstructor(constructorArgType).newInstance(constructorArg) 84 | } catch (e: NoSuchMethodException) { 85 | throw RuntimeException(e) 86 | } catch (e: InstantiationException) { 87 | throw RuntimeException(e) 88 | } catch (e: IllegalAccessException) { 89 | throw RuntimeException(e) 90 | } catch (e: InvocationTargetException) { 91 | throw RuntimeException(e) 92 | } 93 | } -------------------------------------------------------------------------------- /busybee-core/src/main/java/io/americanexpress/busybee/internal/SetMultiMap.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | /** 17 | * Simple purpose-built SetMultiMap designed for the needs of BusyBee. 18 | * We didn't want to have BusyBee depend on guava, so Guava's SetMultiMap wasn't an option. 19 | * Builds on top of mutableMap/mutableSet internally. 20 | * 21 | * 22 | * This collection maps keys of type K to sets of values of type V. 23 | * Each value is unique across all keys in the collection. 24 | * 25 | * 26 | * Not the most efficient impl, but good enough for the BusyBee use case. 27 | * 28 | * 29 | * WARNING: this Impl has some weird quirks, so probably not good for general purpose use. 30 | * Biggest quirk is the at each value is unique across all keys. 31 | * if you want to change the key for a value, you must remove it and re-add it. 32 | * 33 | * 34 | * See the method JavaDocs for details. 35 | */ 36 | class SetMultiMap { 37 | private val map = mutableMapOf>() 38 | private val reverseMap = mutableMapOf() 39 | 40 | /** 41 | * All keys are unique and all values are unique. 42 | * If the Value already exists under any Key, 43 | * then it will not be added again. 44 | * 45 | * 46 | * WARNING: 47 | * To re-associate with another Key, first `removeValue` then re-add the Value 48 | * Else this method will throw an exception. 49 | * 50 | * @return true if a new entry was added. 51 | * @throws IllegalStateException when you add the SAME value again with a DIFFERENT key. 52 | */ 53 | @Throws(IllegalStateException::class) 54 | fun add(key: K, value: V): Boolean { 55 | check(!(reverseMap[value] != null && reverseMap[value] !== key)) { 56 | """ 57 | You can't insert the same value for 2 different keys. 58 | This mapping already exists: 59 | '${reverseMap[value]}' => 60 | '$value' 61 | but you tried to add this new mapping: 62 | '$key' => 63 | '$value' 64 | Remove the old mapping first! 65 | """.trimIndent() 66 | } 67 | if (reverseMap[value] === key) { 68 | return false 69 | } 70 | var valueSet = map[key] 71 | if (valueSet == null) { 72 | valueSet = mutableSetOf() 73 | map[key] = valueSet 74 | } 75 | valueSet.add(value) 76 | reverseMap[value] = key 77 | return true 78 | } 79 | 80 | /** 81 | * Each value ONLY appears once, so it is associated with at most one key. 82 | * 83 | * @return key for which the value is associated with. 84 | */ 85 | fun keyFor(value: V): K? { 86 | return reverseMap[value] 87 | } 88 | 89 | /** 90 | * Removes the value if and only if it exists in the map. 91 | * 92 | * @return true if an entry was removed. 93 | */ 94 | fun removeValue(value: V): Boolean { 95 | if (reverseMap.containsKey(value)) { 96 | val keyForRemoved = reverseMap.remove(value) 97 | ?: throw IllegalStateException("Value not in reverseMap: $value") 98 | (map[keyForRemoved] 99 | ?: throw IllegalStateException("Value removed from reverseMap, but not in map: $value")) 100 | .remove(value) 101 | return true 102 | } 103 | return false 104 | } 105 | 106 | /** 107 | * @return provides an iterator ( with remove support ) for all the values in the collection. 108 | */ 109 | fun valuesIterator(): MutableIterator { 110 | val valueToKeyEntryIterator: MutableIterator> = reverseMap.entries.iterator() 111 | return multiMapIteratorFromReverseMapIterator(valueToKeyEntryIterator) 112 | } 113 | 114 | private fun multiMapIteratorFromReverseMapIterator(reverseMapIterator: MutableIterator>): MutableIterator { 115 | return object : MutableIterator { 116 | private lateinit var reverseEntry: Map.Entry 117 | override fun hasNext(): Boolean { 118 | return reverseMapIterator.hasNext() 119 | } 120 | 121 | override fun next(): V { 122 | reverseEntry = reverseMapIterator.next() 123 | return reverseEntry.key 124 | } 125 | 126 | override fun remove() { 127 | reverseMapIterator.remove() 128 | val setOfValuesFromForwardMap = 129 | requireNotNull(map[reverseEntry.value]) { "Invariant not met. Key not found in forward map." } 130 | setOfValuesFromForwardMap.remove(reverseEntry.key) 131 | } 132 | } 133 | } 134 | 135 | private fun multiMapIteratorFromForwardMapIterator(valueIterator: MutableIterator): MutableIterator { 136 | return object : MutableIterator { 137 | private lateinit var nextValue: V 138 | override fun hasNext(): Boolean { 139 | return valueIterator.hasNext() 140 | } 141 | 142 | override fun next(): V { 143 | nextValue = valueIterator.next() 144 | return nextValue 145 | } 146 | 147 | override fun remove() { 148 | valueIterator.remove() 149 | reverseMap.remove(nextValue) 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * @return All the keys that has ever been used in this map since its creation 156 | */ 157 | fun allKeys(): Set { 158 | return map.keys.toSet() 159 | } 160 | 161 | /** 162 | * @return All values across all keys 163 | */ 164 | fun allValues(): Set { 165 | return reverseMap.keys.toSet() 166 | } 167 | 168 | /** 169 | * @return Values for the given key 170 | */ 171 | fun values(key: K): Set = map[key]?.toSet() ?: emptySet() 172 | 173 | /** 174 | * @return True if and only if there are no values ( there may be keys ) 175 | */ 176 | fun hasNoValues(): Boolean = reverseMap.isEmpty() 177 | 178 | fun valuesIterator(key: K): MutableIterator { 179 | val values: MutableSet = map[key] ?: mutableSetOf() 180 | val valueIterator = values.iterator() 181 | return multiMapIteratorFromForwardMapIterator(valueIterator) 182 | } 183 | 184 | override fun toString(): String { 185 | val sb = StringBuilder() 186 | sb.append("\n{\n") 187 | for ((key, value1) in map) { 188 | sb.append("'").append(key).append("'\n") 189 | val iterator = value1.iterator() 190 | while (iterator.hasNext()) { 191 | val value = iterator.next() 192 | if (iterator.hasNext()) { 193 | sb.append(" ├─ '") 194 | } else { 195 | sb.append(" └─ '") 196 | } 197 | sb.append(value).append("'\n") 198 | } 199 | } 200 | sb.append("}") 201 | return sb.toString() 202 | } 203 | } -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/BusyBeeTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee 15 | 16 | import io.americanexpress.busybee.BusyBee.Category.NETWORK 17 | import io.americanexpress.busybee.BusyBee.NoLongerBusyCallback 18 | import io.americanexpress.busybee.internal.RealBusyBee 19 | import org.assertj.core.api.Java6Assertions.assertThat 20 | import org.junit.Assert.assertFalse 21 | import org.junit.Assert.assertTrue 22 | import org.junit.Assert.fail 23 | import org.junit.Before 24 | import org.junit.Test 25 | import org.mockito.Mockito.mock 26 | import org.mockito.Mockito.verify 27 | import java.util.concurrent.Executor 28 | 29 | class BusyBeeTest { 30 | private lateinit var busyBee: BusyBee 31 | 32 | @Before 33 | fun setUp() { 34 | busyBee = RealBusyBee(IMMEDIATE) 35 | } 36 | 37 | @Test 38 | fun whenBusy_thenIsNotBusyReturnsFalse() { 39 | busyBee.busyWith(this) 40 | 41 | assertIsBusy(busyBee) 42 | } 43 | 44 | @Test 45 | fun whenNetworkBusyAndNotTrackingNetwork_thenIsNotBusy() { 46 | busyBee.busyWith(this, NETWORK) 47 | busyBee.ignoreCategory(NETWORK) 48 | 49 | // should not be busy because we aren't tracking network requests 50 | assertNotBusy(busyBee) 51 | } 52 | 53 | @Test 54 | fun whenNetworkBusyAndPayAttention_thenIsBusy() { 55 | busyBee.busyWith(this, NETWORK) 56 | busyBee.ignoreCategory(NETWORK) 57 | busyBee.payAttentionToCategory(NETWORK) 58 | 59 | assertIsBusy(busyBee) 60 | } 61 | 62 | @Test 63 | fun whenNetworkBusyAndCompletedNetwork_thenIsNotBusy() { 64 | busyBee.busyWith(this, NETWORK) 65 | busyBee.completedEverythingInCategory(NETWORK) 66 | 67 | assertNotBusy(busyBee) 68 | } 69 | 70 | @Test 71 | fun whenCompletedNetwork_thenIsNotBusy() { 72 | busyBee.completedEverythingInCategory(NETWORK) 73 | 74 | assertNotBusy(busyBee) 75 | } 76 | 77 | @Test 78 | fun whenNetworkBusy_thenIsNotBusyReturnsFalse() { 79 | busyBee.busyWith(this, NETWORK) 80 | 81 | assertIsBusy(busyBee) 82 | } 83 | 84 | @Test 85 | fun whenCompleted_thenIsNotBusyReturnsTrue() { 86 | busyBee.busyWith(this) 87 | busyBee.completed(this) 88 | 89 | assertNotBusy(busyBee) 90 | } 91 | 92 | // so we can pass in null as if we are calling from Java 93 | private fun nullForTesting(): Any = null!! 94 | 95 | @Test 96 | fun whenBusyWithNull_thenThrowNullPointerException() { 97 | try { 98 | busyBee.busyWith(nullForTesting()) 99 | fail("busyWith(null) should throw NullPointerException") 100 | } catch (e: NullPointerException) { 101 | // expected 102 | } 103 | } 104 | 105 | @Test 106 | fun whenCompletedNull_thenThrowNullPointerException() { 107 | try { 108 | busyBee.completed(nullForTesting()) 109 | fail("completed(null) should throw NullPointerException") 110 | } catch (e: NullPointerException) { 111 | // expected 112 | } 113 | } 114 | 115 | @Test 116 | fun whenBusyWithSomething_thenNameIncludesSomething() { 117 | busyBee.busyWith("some thing") 118 | 119 | assertThat(busyBee.getName()).contains("some thing") 120 | } 121 | 122 | @Test 123 | fun whenCompletedEverything_thenIsNotBusyReturnsTrue() { 124 | busyBee.busyWith(this) 125 | busyBee.busyWith(Any()) 126 | busyBee.busyWith(Any()) 127 | busyBee.completedEverything() 128 | 129 | assertNotBusy(busyBee) 130 | } 131 | 132 | @Test 133 | fun whenCompleteString_thenOnlyNonStringOperationsRemain() { 134 | val operation = Any() 135 | busyBee.busyWith(operation) 136 | busyBee.busyWith("Network 1", NETWORK) 137 | busyBee.busyWith("General") 138 | busyBee.completedEverythingMatching { o: Any? -> o is String } 139 | 140 | // still busy with 1 non-String operation 141 | assertIsBusy(busyBee) 142 | 143 | busyBee.completed(operation) 144 | 145 | assertNotBusy(busyBee) 146 | } 147 | 148 | @Test 149 | fun whenCompleted_thenNotifyCallback() { 150 | busyBee.busyWith("Op1") 151 | val noLongerBusyCallback = mock(NoLongerBusyCallback::class.java) 152 | busyBee.registerNoLongerBusyCallback(noLongerBusyCallback) 153 | busyBee.completed("Op1") 154 | 155 | verify(noLongerBusyCallback).noLongerBusy() 156 | } 157 | 158 | @Test 159 | fun whenIgnoreRemainingOperations_thenNotifyCallback() { 160 | busyBee.payAttentionToCategory(NETWORK) 161 | busyBee.busyWith("Op1") 162 | busyBee.busyWith("Network", NETWORK) 163 | val noLongerBusyCallback = mock(NoLongerBusyCallback::class.java) 164 | busyBee.registerNoLongerBusyCallback(noLongerBusyCallback) 165 | busyBee.completed("Op1") 166 | busyBee.ignoreCategory(NETWORK) 167 | 168 | verify(noLongerBusyCallback).noLongerBusy() 169 | } 170 | 171 | @Test 172 | fun whenCompletedOnlySome_thenStillBusy() { 173 | val o1 = Any() 174 | val o2 = Any() 175 | assertNotBusy(busyBee) 176 | 177 | busyBee.busyWith(o1) 178 | assertIsBusy(busyBee) 179 | 180 | busyBee.busyWith(o2) 181 | assertIsBusy(busyBee) 182 | 183 | busyBee.completed(o2) 184 | assertIsBusy(busyBee) 185 | 186 | busyBee.completed(o1) 187 | assertNotBusy(busyBee) 188 | } 189 | 190 | companion object { 191 | private val IMMEDIATE: Executor = Executor { obj: Runnable -> obj.run() } 192 | 193 | private fun assertNotBusy(busyBee: BusyBee) { 194 | assertTrue(busyBee.isNotBusy()) 195 | } 196 | 197 | private fun assertIsBusy(busyBee: BusyBee) { 198 | assertFalse(busyBee.isNotBusy()) 199 | } 200 | } 201 | } -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/internal/BusyBeeExecutorWrapperTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import io.americanexpress.busybee.BusyBee.Category.GENERAL 17 | import io.americanexpress.busybee.BusyBee.Category.NETWORK 18 | import io.americanexpress.busybee.BusyBeeExecutorWrapper 19 | import org.assertj.core.api.Java6Assertions.assertThat 20 | import org.junit.Test 21 | import java.util.concurrent.CountDownLatch 22 | import java.util.concurrent.Executor 23 | import java.util.concurrent.ExecutorService 24 | import java.util.concurrent.Executors 25 | import java.util.concurrent.TimeUnit.SECONDS 26 | 27 | class BusyBeeExecutorWrapperTest { 28 | @Test 29 | fun whenExecutorWrappedForNoOp_thenItDoesNotGetWrapped() { 30 | val originalExecutor = Executor { obj: Runnable -> obj.run() } 31 | val wrappedExecutor = 32 | BusyBeeExecutorWrapper.with(NoOpBusyBee()) 33 | .wrapExecutor(originalExecutor) 34 | .build() 35 | assertThat(wrappedExecutor === originalExecutor) 36 | .`as`("Expected wrapped executor to equal original executor for NoOpBusyBee") 37 | .isTrue() 38 | } 39 | 40 | @Test 41 | fun whenWrappedWithBusyBeeExecutorWrapper_thenBusyBeeSaysItIsBusy() { 42 | val mainThread = MainThread.singletonExecutor as ExecutorService 43 | val busyBee = RealBusyBee(mainThread) 44 | val wrappedThread = Executors.newSingleThreadExecutor() 45 | val executorWrapper = 46 | BusyBeeExecutorWrapper.with(busyBee) 47 | .executeInCategory(NETWORK) 48 | .wrapExecutor(wrappedThread) 49 | .build() 50 | 51 | val busyLatch = CountDownLatch(1) 52 | executorWrapper.execute { 53 | try { 54 | busyLatch.await() 55 | } catch (e: InterruptedException) { 56 | throw RuntimeException(e) 57 | } 58 | } 59 | 60 | // shouldn't complete anything because our executor uses NETWORK category 61 | busyBee.completedEverythingInCategory(GENERAL) 62 | 63 | assertThat(busyBee.isBusy()) 64 | .`as`("Should be \"busy\" waiting for Latch") 65 | .isTrue() 66 | assertThat(busyBee.isNotBusy()) 67 | .`as`("Should be \"busy\" waiting for Latch") 68 | .isFalse() 69 | 70 | busyLatch.countDown() 71 | 72 | // wrappedThread will post a Runnable to MainThread to tell BusyBee it completed. 73 | wrappedThread.shutdown() 74 | wrappedThread.awaitTermination(10, SECONDS) 75 | 76 | // wait for the completion message from the wrapped thread to run on the main thread. 77 | mainThread.shutdown() 78 | mainThread.awaitTermination(10, SECONDS) 79 | 80 | assertThat(busyBee.isBusy()) 81 | .`as`("Should no longer be \"busy\" waiting for Latch") 82 | .isFalse() 83 | assertThat(busyBee.isNotBusy()) 84 | .`as`("Should no longer be \"busy\" waiting for Latch") 85 | .isTrue() 86 | } 87 | } -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/internal/BusyBeeSingletonTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent 18 | import io.americanexpress.busybee.internal.EnvironmentChecks.doNotPretendTestsAreNotRunning 19 | import io.americanexpress.busybee.internal.EnvironmentChecks.pretendTestsAreNotRunning 20 | import org.assertj.core.api.Java6Assertions.assertThat 21 | import org.junit.Test 22 | 23 | class BusyBeeSingletonTest { 24 | @Test 25 | fun whenInJvmTest_espressoTestsAreNotRunning() { 26 | assertThat(androidJunitRunnerIsPresent()).isFalse() 27 | } 28 | 29 | @Test 30 | fun whenInJvmTest_useRealBusyBee() { 31 | assertThat(BusyBeeSingleton.create()).isInstanceOf(RealBusyBee::class.java) 32 | } 33 | 34 | @Test 35 | fun whenTestsAreNotRunning_useNoOpBusyBee() { 36 | pretendTestsAreNotRunning() 37 | assertThat(BusyBeeSingleton.create()).isInstanceOf(NoOpBusyBee::class.java) 38 | doNotPretendTestsAreNotRunning() 39 | } 40 | } -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/internal/EnvironmentChecksTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import io.americanexpress.busybee.internal.EnvironmentChecks.androidJunitRunnerIsPresent 18 | import io.americanexpress.busybee.internal.EnvironmentChecks.hasWorkingAndroidMainLooper 19 | import io.americanexpress.busybee.internal.EnvironmentChecks.testsAreRunning 20 | import org.assertj.core.api.Java6Assertions.assertThat 21 | import org.junit.Test 22 | 23 | class EnvironmentChecksTest { 24 | @Test 25 | fun testsAreRunningShouldAlwaysBeTrueInATest() { 26 | assertThat(testsAreRunning()).isTrue() 27 | } 28 | 29 | @Test 30 | fun whenInNonAndroidModule_thenEspressoIdlingResourceIsPresentIsFalse() { 31 | assertThat(androidJunitRunnerIsPresent()).isFalse() 32 | } 33 | 34 | @Test 35 | fun whenInNonAndroidModule_thenIsAndroidReturnsFalse() { 36 | assertThat(hasWorkingAndroidMainLooper()).isFalse() 37 | } 38 | } -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/internal/MainThreadTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import org.assertj.core.api.Java6Assertions.assertThat 18 | import org.junit.Test 19 | 20 | class MainThreadTest { 21 | @Test 22 | fun whenOnJvm_thenWeGetTheNonAndroidExecutor() { 23 | val executor = MainThread.singletonExecutor 24 | assertThat(executor.javaClass.name) 25 | .isNotEqualTo("io.americanexpress.busybee.android.internal.AndroidMainThreadExecutor") 26 | } 27 | } -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/internal/NoOpBusyBeeTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import io.americanexpress.busybee.BusyBee.Category.GENERAL 17 | import io.americanexpress.busybee.BusyBee.Category.NETWORK 18 | import org.assertj.core.api.Java6Assertions.assertThat 19 | import org.junit.Test 20 | 21 | class NoOpBusyBeeTest { 22 | @Test 23 | fun whenNoOpBusyWith_thenNotBusy() { 24 | val busyBee = NoOpBusyBee() 25 | busyBee.busyWith("SOMETHING") 26 | 27 | assertThat(busyBee.isNotBusy()).isTrue() 28 | assertThat(busyBee.isBusy()).isFalse() 29 | } 30 | 31 | @Test 32 | fun whenNoOpBusyWith_thenNothingThrowsException() { 33 | val busyBee = NoOpBusyBee() 34 | busyBee.busyWith("SOMETHING") 35 | busyBee.completed("SOMETHING") 36 | busyBee.payAttentionToCategory(NETWORK) 37 | busyBee.ignoreCategory(NETWORK) 38 | busyBee.completedEverything() 39 | busyBee.completedEverythingInCategory(GENERAL) 40 | 41 | assertThat(busyBee.isNotBusy()).isTrue() 42 | assertThat(busyBee.isBusy()).isFalse() 43 | assertThat(busyBee.getName()).isNotNull() 44 | assertThat(busyBee.toStringVerbose()).isNotNull() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/internal/ReflectionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package io.americanexpress.busybee.internal 16 | 17 | import io.americanexpress.busybee.internal.Reflection.classIsFound 18 | import org.assertj.core.api.Java6Assertions.assertThat 19 | import org.junit.Test 20 | 21 | class ReflectionTest { 22 | @Test 23 | fun canFindThisClass() { 24 | assertThat(classIsFound("io.americanexpress.busybee.internal.ReflectionTest")) 25 | .`as`("must be able to find this class") 26 | .isTrue() 27 | } 28 | 29 | @Test 30 | fun cantFindNonExistentClass() { 31 | assertThat(classIsFound("com.doesnt.exist.Class")) 32 | .`as`("That class should not exist") 33 | .isFalse() 34 | } 35 | } -------------------------------------------------------------------------------- /busybee-core/src/test/java/io/americanexpress/busybee/internal/SetMultiMapTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.internal 15 | 16 | import org.assertj.core.api.Java6Assertions.assertThat 17 | import org.assertj.core.api.Java6Assertions.fail 18 | import org.junit.Before 19 | import org.junit.Test 20 | 21 | class SetMultiMapTest { 22 | private lateinit var map: SetMultiMap 23 | 24 | @Before 25 | fun setUp() { 26 | map = SetMultiMap() 27 | } 28 | 29 | @Test 30 | fun whenValueAdd_thenCanBeRetrieved() { 31 | map.add(1, "1") 32 | map.add(1, "2") 33 | map.add(2, "4") 34 | map.add(2, "3") 35 | 36 | assertThat(map.values(1)) 37 | .containsExactlyInAnyOrder("1", "2") 38 | assertThat(map.values(2)) 39 | .containsExactlyInAnyOrder("4", "3") 40 | assertThat(map.allValues()) 41 | .containsExactlyInAnyOrder("1", "2", "3", "4") 42 | } 43 | 44 | @Test 45 | fun whenValueAddedFirstTime_thenReturnTrue() { 46 | val wasAdded = map.add(1, "2") 47 | 48 | assertThat(wasAdded).isTrue() 49 | } 50 | 51 | @Test 52 | fun whenKeyValueAddedSecondTime_thenReturnFalse() { 53 | map.add(2, "2") 54 | val resultForSecondAdd = map.add(2, "2") 55 | 56 | assertThat(resultForSecondAdd).isFalse() 57 | } 58 | 59 | @Test 60 | fun whenNoValues_thenReturnEmptySet() { 61 | assertThat(map.values(1)).isEmpty() 62 | } 63 | 64 | @Test 65 | fun whenKeyFor_thenReturnValue() { 66 | map.add(1, "1") 67 | map.add(2, "3") 68 | map.add(1, "2") 69 | 70 | assertThat(map.keyFor("1")).isEqualTo(1) 71 | } 72 | 73 | @Test 74 | fun whenAddSameValue_thenThrowException() { 75 | try { 76 | map.add(1, "1") 77 | map.add(2, "1") 78 | fail("Adding the same value twice must throw an exception") 79 | } catch (e: IllegalStateException) { 80 | // expected 81 | } 82 | } 83 | 84 | @Test 85 | fun whenRemoveValue_thenCanAddBackWithDifferentKey() { 86 | map.add(1, "1") 87 | map.add(1, "3") 88 | map.removeValue("1") 89 | map.add(2, "1") 90 | map.add(1, "2") 91 | 92 | assertThat(map.keyFor("1")).isEqualTo(2) 93 | } 94 | 95 | @Test 96 | fun whenRemoveTwice_thenReturnFalseOnSecondTime() { 97 | map.add(1, "1") 98 | map.add(2, "2") 99 | val resultWhenValuePresent = map.removeValue("1") 100 | val resultAfterValueRemoved = map.removeValue("1") 101 | 102 | assertThat(resultWhenValuePresent).isTrue() 103 | assertThat(resultAfterValueRemoved).isFalse() 104 | } 105 | 106 | @Test 107 | fun whenAllKeys_thenAllKeysArePresent() { 108 | map.add(1, "1") 109 | map.add(2, "2") 110 | map.add(2, "3") 111 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2) 112 | } 113 | 114 | @Test 115 | fun whenAllValues_thenAllValuesArePresent() { 116 | map.add(1, "1") 117 | map.add(2, "2") 118 | map.add(2, "3") 119 | 120 | assertThat(map.allValues()).containsExactlyInAnyOrder("1", "2", "3") 121 | } 122 | 123 | @Test 124 | fun valuesReturnsOnlyValuesForTheSpecifiedKey() { 125 | map.add(1, "1") 126 | map.add(2, "2") 127 | map.add(2, "3") 128 | 129 | assertThat(map.values(2)).containsExactlyInAnyOrder("2", "3") 130 | } 131 | 132 | @Test 133 | fun whenAllValuesRemoved_ThenIsEmpty() { 134 | map.add(1, "1") 135 | map.removeValue("1") 136 | 137 | assertThat(map.hasNoValues()).isTrue() 138 | } 139 | 140 | @Test 141 | fun whenHasValue_ThenNotIsEmpty() { 142 | map.add(1, "1") 143 | 144 | assertThat(map.hasNoValues()).isFalse() 145 | } 146 | 147 | @Test 148 | fun whenIterateRemove_ThenIsActuallyRemoved() { 149 | map.add(1, "A") 150 | map.add(1, "B") 151 | map.add(2, "C") 152 | map.add(2, "C") 153 | map.add(3, "E") 154 | map.add(3, "D") 155 | val stringIterator = map.valuesIterator() 156 | while (stringIterator.hasNext()) { 157 | val next = stringIterator.next() 158 | if (next == "C") { 159 | stringIterator.remove() 160 | } 161 | } 162 | 163 | assertThat(map.allValues()).containsExactlyInAnyOrder("A", "B", "D", "E") 164 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3) 165 | assertThat(map.values(2)).isEmpty() 166 | } 167 | 168 | @Test 169 | fun whenIterateValuesForKeyAndRemove_ThenIsActuallyRemoved() { 170 | map.add(1, "A") 171 | map.add(1, "B") 172 | map.add(2, "X") 173 | map.add(2, "Y") 174 | map.add(3, "E") 175 | map.add(3, "C") 176 | map.add(3, "D") 177 | val stringIterator = map.valuesIterator(3) 178 | while (stringIterator.hasNext()) { 179 | val next = stringIterator.next() 180 | if (next == "C") { 181 | stringIterator.remove() 182 | } 183 | } 184 | assertThat(map.allValues()).containsExactlyInAnyOrder("A", "B", "X", "E", "Y", "D") 185 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3) 186 | assertThat(map.values(3)).containsExactlyInAnyOrder("D", "E") 187 | } 188 | 189 | @Test 190 | fun whenIterateValuesForKeyWithNoValues_thenNoExceptionThrown() { 191 | map.add(1, "A") 192 | map.add(1, "B") 193 | map.add(2, "X") 194 | map.add(2, "Y") 195 | map.add(3, "E") 196 | map.add(3, "C") 197 | map.add(3, "D") 198 | val stringIterator = map.valuesIterator(4) 199 | while (stringIterator.hasNext()) { 200 | val next = stringIterator.next() 201 | if (next == "C") { 202 | stringIterator.remove() 203 | } 204 | } 205 | assertThat(map.allValues()).containsExactlyInAnyOrder("A", "C", "B", "X", "E", "Y", "D") 206 | assertThat(map.allKeys()).containsExactlyInAnyOrder(1, 2, 3) 207 | assertThat(map.values(3)).containsExactlyInAnyOrder("D", "E", "C") 208 | } 209 | 210 | @Test 211 | fun toString_thenPrintSomethingReasonable() { 212 | map.add(1, "A") 213 | map.add(1, "B") 214 | map.add(2, "X") 215 | map.add(2, "Y") 216 | map.add(3, "E") 217 | map.add(3, "C") 218 | map.add(3, "D") 219 | 220 | assertThat(map.toString()).isEqualTo( 221 | "\n" + """ 222 | { 223 | '1' 224 | ├─ 'A' 225 | └─ 'B' 226 | '2' 227 | ├─ 'X' 228 | └─ 'Y' 229 | '3' 230 | ├─ 'E' 231 | ├─ 'C' 232 | └─ 'D' 233 | }""".trimIndent() 234 | ) 235 | 236 | } 237 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 American Express Travel Related Services Company, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | # in compliance with the License. You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License 10 | # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | # or implied. See the License for the specific language governing permissions and limitations under 12 | # the License. 13 | # 14 | android.useAndroidX=true 15 | systemProp.group=io.americanexpress.busybee 16 | systemProp.version=0.0.5 17 | kotlin.code.style=official 18 | # Dokka needs more memory than the default provides. 19 | org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m 20 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | compileSdk = "android-29" 3 | targetSdk = "29" 4 | minimumSdk = "19" 5 | kotlin = "1.6.0" 6 | junit = "4.13.1" 7 | assertj = "2.6.0" 8 | mockito2 = "3.0.0" 9 | androidx_material = "1.0.0" 10 | androidx_annotation = "1.1.0" 11 | androidx_test_rules = "1.2.0" 12 | androidx_espresso = "3.2.0" 13 | 14 | [libraries] 15 | kotlin_stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } 16 | junit4 = { module = "junit:junit", version.ref = "junit" } 17 | assertj_core = { module = "org.assertj:assertj-core", version.ref = "assertj" } 18 | mockito_core = { module = "org.mockito:mockito-core", version.ref = "mockito2" } 19 | androidx_test_rules = { module = "androidx.test:rules", version.ref = "androidx_test_rules" } 20 | androidx_material = { module = "com.google.android.material:material", version.ref = "androidx_material" } 21 | androidx_annotation = { module = "androidx.annotation:annotation", version.ref = "androidx_annotation" } 22 | androidx_espresso_core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx_espresso" } 23 | androidx_espresso_idling_resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "androidx_espresso" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/americanexpress/busybee/28925c25afc90fb7e185879d4f9e621350efedb8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 American Express Travel Related Services Company, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | # in compliance with the License. You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software distributed under the License 10 | # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | # or implied. See the License for the specific language governing permissions and limitations under 12 | # the License. 13 | # 14 | distributionBase=GRADLE_USER_HOME 15 | distributionPath=wrapper/dists 16 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip 17 | zipStoreBase=GRADLE_USER_HOME 18 | zipStorePath=wrapper/dists 19 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /images/busybee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/americanexpress/busybee/28925c25afc90fb7e185879d4f9e621350efedb8/images/busybee.png -------------------------------------------------------------------------------- /sample-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample-app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | plugins { 16 | id("com.android.application") 17 | id("org.jetbrains.kotlin.android") 18 | } 19 | 20 | group = "io.americanexpress.busybee.app" 21 | 22 | android { 23 | compileSdkVersion = libs.versions.compileSdk.get() 24 | defaultConfig { 25 | minSdk = libs.versions.minimumSdk.get().toInt() 26 | targetSdk = libs.versions.targetSdk.get().toInt() 27 | version = 1 28 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 29 | } 30 | 31 | lint { 32 | isWarningsAsErrors = true 33 | disable("GoogleAppIndexingWarning", // not needed for a sample app 34 | "MissingApplicationIcon") // not needed for a sample app 35 | } 36 | 37 | buildTypes { 38 | getByName("release") { 39 | signingConfig = signingConfigs.getByName("debug") 40 | isMinifyEnabled = false 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation(project(":busybee-android")) 47 | implementation(libs.androidx.material) 48 | implementation(libs.kotlin.stdlib) 49 | 50 | androidTestImplementation(libs.androidx.annotation) 51 | androidTestImplementation(libs.androidx.espresso.core) 52 | androidTestImplementation(libs.androidx.espresso.idling.resource) 53 | androidTestImplementation(libs.androidx.test.rules) 54 | androidTestImplementation(libs.assertj.core) 55 | } 56 | -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/io/americanexpress/busybee/BusyBeeActivityTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee 15 | 16 | import androidx.test.espresso.Espresso.onView 17 | import androidx.test.espresso.action.ViewActions.click 18 | import androidx.test.espresso.assertion.ViewAssertions.matches 19 | import androidx.test.espresso.matcher.ViewMatchers.withId 20 | import androidx.test.espresso.matcher.ViewMatchers.withText 21 | import androidx.test.rule.ActivityTestRule 22 | import io.americanexpress.busybee.sample.BusyBeeActivity 23 | import org.junit.Before 24 | import org.junit.Test 25 | 26 | class BusyBeeActivityTest { 27 | private val activityRule = ActivityTestRule(BusyBeeActivity::class.java) 28 | 29 | @Before 30 | fun setUp() { 31 | activityRule.launchActivity(null) 32 | } 33 | 34 | @Test 35 | fun whenClickButton_thenEspressoWaitsForResultToBeDisplayed() { 36 | onView(withText(R.string.press_me)).perform(click()) 37 | onView(withId(R.id.confirmation)).check(matches(withText("Button Pressed"))) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /sample-app/src/androidTest/java/io/americanexpress/busybee/BusyBeeCategoryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee 15 | 16 | import android.os.Handler 17 | import android.os.Looper 18 | import android.view.View 19 | import android.widget.TextView 20 | import androidx.test.espresso.Espresso.onView 21 | import androidx.test.espresso.PerformException 22 | import androidx.test.espresso.action.ViewActions.click 23 | import androidx.test.espresso.assertion.ViewAssertions.matches 24 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 25 | import androidx.test.espresso.matcher.ViewMatchers.withText 26 | import androidx.test.rule.ActivityTestRule 27 | import io.americanexpress.busybee.BusyBee.Category.NETWORK 28 | import io.americanexpress.busybee.sample.BusyBeeActivity 29 | import org.junit.Assert.fail 30 | import org.junit.Before 31 | import org.junit.Test 32 | 33 | class BusyBeeCategoryTest { 34 | 35 | private val activityRule = ActivityTestRule(BusyBeeActivity::class.java) 36 | private val busyBee = BusyBee.singleton() 37 | 38 | companion object { 39 | const val idleTimeoutInMillis = 4000L 40 | const val SOME_BACKGROUND_OPERATION = "Some background Operation" 41 | const val NETWORK_BACKGROUND_OPERATION = "Network background Operation" 42 | 43 | private val MAIN_THREAD = Handler(Looper.getMainLooper()) 44 | } 45 | 46 | @Before 47 | fun setUp() { 48 | val activity = activityRule.launchActivity(null) 49 | 50 | activity.onButtonClick(View.OnClickListener { view: View -> 51 | when (view) { 52 | is TextView -> { 53 | busyBee.busyWith(SOME_BACKGROUND_OPERATION) 54 | busyBee.busyWith(NETWORK_BACKGROUND_OPERATION, NETWORK) 55 | Thread(Runnable { 56 | try { 57 | Thread.sleep(idleTimeoutInMillis / 2) 58 | MAIN_THREAD.post { view.text = "All done." } 59 | } catch (e: InterruptedException) { 60 | throw RuntimeException(e) 61 | } finally { 62 | busyBee.completed(SOME_BACKGROUND_OPERATION) 63 | // Purposefully, don't complete the Network operation 64 | // busyBee.completed(NETWORK_BACKGROUND_OPERATION) 65 | } 66 | }).start() 67 | } 68 | else -> throw IllegalArgumentException() 69 | } 70 | }) 71 | 72 | } 73 | 74 | @Test 75 | fun whenPayAttentionToNetwork_thenExceptionWhenClick() { 76 | busyBee.payAttentionToCategory(NETWORK) 77 | try { 78 | onView(withText(R.string.press_me)).perform(click()) 79 | fail("should throw a perform exception because the network operation never completes") 80 | } catch (e: PerformException) { 81 | // expected 82 | } 83 | } 84 | 85 | @Test 86 | fun whenIgnoreNetwork_thenCanStillClickButton() { 87 | busyBee.ignoreCategory(NETWORK) 88 | 89 | onView(withText(R.string.press_me)).perform(click()) 90 | onView(withText("All done.")).check(matches(isDisplayed())) 91 | } 92 | } -------------------------------------------------------------------------------- /sample-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 17 | 18 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample-app/src/main/java/io/americanexpress/busybee/sample/BusyBeeActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 American Express Travel Related Services Company, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package io.americanexpress.busybee.sample 15 | 16 | import android.os.Bundle 17 | import android.view.View 18 | import android.widget.Button 19 | import android.widget.TextView 20 | import androidx.appcompat.app.AppCompatActivity 21 | import io.americanexpress.busybee.BusyBee 22 | import io.americanexpress.busybee.R 23 | import java.util.concurrent.Executors 24 | 25 | class BusyBeeActivity : AppCompatActivity() { 26 | private val busyBee = BusyBee.singleton() 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_busy_bee) 31 | val confirmation = findViewById(R.id.confirmation) 32 | onButtonClick(View.OnClickListener { 33 | val foo = "FOO" 34 | busyBee.busyWith(foo) 35 | Executors.newSingleThreadExecutor().execute { 36 | Thread.sleep(1000) 37 | confirmation.post { 38 | confirmation.text = getString(R.string.button_pressed) 39 | busyBee.completed(foo) 40 | } 41 | } 42 | }) 43 | } 44 | 45 | fun onButtonClick(clickListener: View.OnClickListener) { 46 | val button = findViewById