diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..83be1d5a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +# https://editorconfig.org/ +# This configuration is used by ktlint when spotless invokes it + +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma=true +ij_kotlin_allow_trailing_comma_on_call_site=true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..3372a657 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,59 @@ +name: Bug Report +description: Create a report of specific sample to help us improve +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + - type: dropdown + id: area + attributes: + label: In which area is the issue? + multiple: false + options: + - General + - Accessibility + - Camera + - Connectivity + - Location + - Privacy + - User Interface + validations: + required: true + - type: input + id: sample-path + attributes: + label: In a specific sample? + description: Please add the sample entry point file name of the affected sample + placeholder: e.g SinglePermission.kt + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? and provide screen captures if possible + placeholder: Tell us what you see! + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant logcat output + description: Please copy and paste any relevant logcat output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..64aa9529 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,53 @@ +name: Feature request +description: File a feature request +title: "[FR]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this report! + - type: checkboxes + attributes: + label: Is there an existing FR for this? + description: Please search to see if a similar FR already exists + options: + - label: I have searched the existing issues + required: true + - type: dropdown + id: area + attributes: + label: For which area is this FR? + multiple: false + options: + - General + - Accessibility + - Camera + - Connectivity + - Location + - Privacy + - User Interface + validations: + required: true + - type: input + id: sample-path + attributes: + label: Is it for a specific sample? + description: Please add the sample entry point file name of the affected sample + placeholder: e.g SinglePermission.kt + - type: textarea + id: what-happened + attributes: + label: Describe the feature request + description: Be clear about the problem and possible solutions + placeholder: I want to... + validations: + required: true + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..0aa3cc97 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,14 @@ +--- +name: Pull request +about: Create a pull request +--- +Thank you for opening a Pull Request! +Before submitting your PR, there are a few things you can do to make sure it goes smoothly: +- [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Ensure the tests and linter pass (`./gradlew --init-script gradle/spotless-init.gradle.kts spotlessApply` to automatically apply formatting) + +Is this your first Pull Request? +- [ ] Run `./tools/setup.sh` +- [ ] Import the code formatting style as explained in [the setup script](/tools/setup.sh#L40). + +Fixes # 🦕 diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties new file mode 100644 index 00000000..93bc529c --- /dev/null +++ b/.github/ci-gradle.properties @@ -0,0 +1,28 @@ +# +# Copyright 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.gradle.daemon=false +org.gradle.parallel=true +org.gradle.workers.max=2 + +kotlin.incremental=false +kotlin.compiler.execution.strategy=in-process + +# Controls KotlinOptions.allWarningsAsErrors. +# This value used in CI and is currently set to false. +# If you want to treat warnings as errors locally, set this property to true +# in your ~/.gradle/gradle.properties file. +warningsAsErrors=false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..1fed3139 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,118 @@ +name: Build + +on: + push: + branches: + - main + pull_request: +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Check spotless + run: ./gradlew spotlessCheck --init-script gradle/spotless-init.gradle.kts --no-configuration-cache + + - name: Check lint + run: ./gradlew lintDebug + + - name: Build debug + run: ./gradlew assembleDebug + + - name: Run local tests + run: ./gradlew testDebug + + - name: Upload build outputs (APKs) + uses: actions/upload-artifact@v3 + with: + name: build-outputs + path: app/build/outputs + + - name: Upload build reports + if: always() + uses: actions/upload-artifact@v3 + with: + name: build-reports + path: app/build/reports + + androidTest: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 55 + strategy: + matrix: + api-level: [34] + + steps: + - name: Delete unnecessary tools 🔧 + uses: jlumbroso/free-disk-space@v1.3.1 + with: + android: false # Don't remove Android tools + tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY" + dotnet: true # rm -rf /usr/share/dotnet + haskell: true # rm -rf /opt/ghc... + swap-storage: true # rm -f /mnt/swapfile (4GiB) + docker-images: false # Takes 16s, enable if needed in the future + large-packages: false # includes google-cloud-sdk and it's slow + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + + - name: Checkout + uses: actions/checkout@v4 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Run instrumentation tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + disable-animations: true + disk-size: 6000M + heap-size: 600M + script: ./gradlew connectedDebugAndroidTest --daemon + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v3 + with: + name: test-reports-${{ matrix.api-level }} + path: '*/build/reports/androidTests' diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml new file mode 100644 index 00000000..926a3e2a --- /dev/null +++ b/.github/workflows/issues-stale.yml @@ -0,0 +1,16 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '15 3 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' + days-before-stale: 30 + days-before-close: 5 + exempt-all-pr-milestones: true + exempt-issue-labels: 'waiting for info,waiting on dependency' diff --git a/.gitignore b/.gitignore index 7df8dff5..fd9488be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,56 @@ -# generated files -bin/ -gen/ +# Gradle +.gradle +build/ -# Local configuration file (sdk path, etc) -project.properties -.settings/ +captures + +**/local.properties + +# IntelliJ .idea folder +.idea/workspace.xml +.idea/libraries +.idea/caches +.idea/navEditor.xml +.idea/tasks.xml +.idea/modules.xml +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/deploymentTargetDropDown.xml +.idea/deploymentTargetSelector.xml +.idea/misc.xml +.idea/androidTestResultsUserPreferences.xml +.idea/inspectionProfiles +.idea/vcs.xml + +# Ignore all auto-generated configs except for app.xml. It's needed as template for the build +.idea/runConfigurations +!.idea/runConfigurations/app.xml + +gradle.xml +*.iml + +# General +.DS_Store +.externalNativeBuild + +# Do not commit plain-text signing info +release/*.properties +release/*.gpg + +# VS Code config +org.eclipse.buildship.core.prefs .classpath .project + +# Temporary API docs +docs/api +package-list-coil-base + +# Mkdocs temporary serving folder +docs-gen +site +*.bak +.idea/appInsightsSettings.xml + +#TFLite +*.tflite diff --git a/.google/packaging.yaml b/.google/packaging.yaml new file mode 100644 index 00000000..2aacc257 --- /dev/null +++ b/.google/packaging.yaml @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# GOOGLE SAMPLE PACKAGING DATA +# +# This file is used by Google as part of our samples packaging process. +# End users may safely ignore this file. It has no relevance to other systems. +--- +status: PUBLISHED +technologies: [Android, Platform] +categories: + - Permissions + - Camera + - Accessibility + - Connectivity + - Location + - Privacy + - UserInterface + - AndroidPlatform + - JetpackComposeA11y + - JetpackComposePermissions +solutions: + - Mobile + - Platform + - SDK + - Jetpack +languages: [Kotlin] +github: android/platform-samples +level: INTERMEDIATE +license: apache2 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..43514b5e --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Platform Samples \ No newline at end of file diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml new file mode 100644 index 00000000..1661ec9d --- /dev/null +++ b/.idea/assetWizardSettings.xml @@ -0,0 +1,45 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..ab75be24 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,124 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/AOSP.xml b/.idea/copyright/AOSP.xml new file mode 100644 index 00000000..58b8ac7e --- /dev/null +++ b/.idea/copyright/AOSP.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..d6e60d48 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 00000000..8d81632f --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..f8b12cb5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,63 @@ +# Google Open Source Community Guidelines + +At Google, we recognize and celebrate the creativity and collaboration of open +source contributors and the diversity of skills, experiences, cultures, and +opinions they bring to the projects and communities they participate in. + +Every one of Google's open source projects and communities are inclusive +environments, based on treating all individuals respectfully, regardless of +gender identity and expression, sexual orientation, disabilities, +neurodiversity, physical appearance, body size, ethnicity, nationality, race, +age, religion, or similar personal characteristic. + +We value diverse opinions, but we value respectful behavior more. + +Respectful behavior includes: + +* Being considerate, kind, constructive, and helpful. +* Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or + physically threatening behavior, speech, and imagery. +* Not engaging in unwanted physical contact. + +Some Google open source projects [may adopt][] an explicit project code of +conduct, which may have additional detailed expectations for participants. Most +of those projects will use our [modified Contributor Covenant][]. + +[may adopt]: https://opensource.google/docs/releasing/preparing/#conduct +[modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ + +## Resolve peacefully + +We do not believe that all conflict is necessarily bad; healthy debate and +disagreement often yields positive results. However, it is never okay to be +disrespectful. + +If you see someone behaving disrespectfully, you are encouraged to address the +behavior directly with those involved. Many issues can be resolved quickly and +easily, and this gives people more control over the outcome of their dispute. +If you are unable to resolve the matter for any reason, or if the behavior is +threatening or harassing, report it. We are dedicated to providing an +environment where participants feel welcome and safe. + +## Reporting problems + +Some Google open source projects may adopt a project-specific code of conduct. +In those cases, a Google employee will be identified as the Project Steward, +who will receive and handle reports of code of conduct violations. In the event +that a project hasn’t identified a Project Steward, you can report problems by +emailing opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is +taken. The identity of the reporter will be omitted from the details of the +report supplied to the accused. In potentially harmful situations, such as +ongoing harassment or threats to anyone's safety, we may take action without +notice. + +*This document was adapted from the [IndieWeb Code of Conduct][] and can also +be found at .* + +[IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ebbb59e5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3ba6ad3f --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +![Build](https://github.com/android/platform-samples/actions/workflows/build.yml/badge.svg) + +# Android Platform Samples + +This repository contains a collection of samples that demonstrate the use of different Android OS platform APIs. The samples are organized into folders by topic, and each folder contains a README file that provides more information about the samples in that folder. + +> **Note:** These samples are intended to showcase specific functionality in isolation, and they may use +> simplified code. They are not intended to be used as production-ready code. The project uses the +> [casa-android](https://github.com/google/casa-android) (intended only for demo projects). +> For best practices follow our documentation and check +> [Now In Android](https://github.com/android/nowinandroid) + +Browse the samples inside each topic samples folder: + +- [Accessibility](https://github.com/android/platform-samples/tree/main/samples/accessibility) +- [Camera](https://github.com/android/platform-samples/tree/main/samples/camera) +- [Connectivity](https://github.com/android/platform-samples/tree/main/samples/connectivity) +- [Graphics](https://github.com/android/platform-samples/tree/main/samples/graphics) +- [Location](https://github.com/android/platform-samples/tree/main/samples/location) +- [Privacy](https://github.com/android/platform-samples/tree/main/samples/privacy) +- [Storage](https://github.com/android/platform-samples/tree/main/samples/storage) +- [User-interface](https://github.com/android/platform-samples/tree/main/samples/user-interface) +- More to come... + +We are constantly adding new samples to this repository. You can find a list of all the available samples [here](https://github.com/android/platform-samples/tree/main/samples/README.md). + +> 🚧 **Work-in-Progress:** we are working on bringing more existing and new samples into this format. + +## How to run + +1. Clone the repository +2. Open the whole project in Android Studio. +3. Sync & Run `app` configuration + +The app will open with the samples list screen that allows you to navigate throughout the different +categories and available samples. + +> **Note:** the `app` module is required to bring together all the samples but it's not relevant +> for their functionality, you can simply ignore it. The wiring is done under the hood and an +> implementation detail not needed to understand any sample functionality. + +### Deeplink to sample + +To open a specific sample directly you can use one of the auto-generated configurations. + +1. Build the project at least once +2. Open `Run Configuration` dropdown +3. Select sample name +4. Run + +> **Tip:** use `⌃⌥R` or `Alt+Shift+F10` shortcut to open the full list and launch the selected one. + +## Reporting Issues + +You can report an [issue with a sample](https://github.com/android/platform-samples/issues) using +this repository. When doing so, make sure to specify which sample you are referring to. + +## Contributions + +Please contribute! We will gladly review any pull requests. +Make sure to read the [Contributing](CONTRIBUTING.md) page first though. + +> Note: make sure to run `./gradlew --init-script gradle/spotless-init.gradle.kts spotlessApply` before +> submitting PRs. + +## License + +``` +Copyright 2023 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/README.md b/app/README.md new file mode 100644 index 00000000..0a7a4aec --- /dev/null +++ b/app/README.md @@ -0,0 +1,24 @@ +# Platform Samples app + +This module contains the necessary infrastructure using the [casa-android framework](https://github.com/google/casa-android) +to wire all the samples together and offer a launch point. + +**You can ignore the code in this modules since it's not relevant for the samples** + +## License + +``` +Copyright 2023 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..4e383b1e --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,115 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) + id("com.example.platform.convention") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +android { + namespace = "com.example.platform.app" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.platform.app" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "com.example.platform.app.AppTestRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() + } + packagingOptions { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(platform(libs.compose.bom)) + implementation(libs.casa.ui) + implementation(libs.androidx.appcompat) + + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + implementation(libs.coil.base) + implementation(libs.coil.video) + + // include all available samples. + val samples: List by project.extra + samples.forEach { + implementation(project(it)) + } + + // Testing + kspAndroidTest(libs.hilt.compiler) + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.androidx.navigation.testing) + androidTestImplementation(libs.compose.ui.test.manifest) + debugImplementation(libs.compose.ui.test.manifest) + androidTestImplementation(libs.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.hilt.testing) + androidTestImplementation(libs.junit4) +} + +tasks.register("syncSamplesInfo", com.example.platform.plugin.SyncSamplesInfo::class.java) { + projectDir.set(project.rootDir) +} +// Link the assemble task to the sync task. +// TODO: move syncSamplesInfo task here, as this can break isolated projects +afterEvaluate { + tasks.findByName("assembleDebug")?.finalizedBy(tasks.findByName("syncSamplesInfo")) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/platform/app/AppTestRunner.kt b/app/src/androidTest/java/com/example/platform/app/AppTestRunner.kt new file mode 100644 index 00000000..4b5ad9c8 --- /dev/null +++ b/app/src/androidTest/java/com/example/platform/app/AppTestRunner.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.app + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +/** + * A custom runner to set up the instrumented application class for tests. + */ +class AppTestRunner : AndroidJUnitRunner() { + override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} diff --git a/app/src/androidTest/java/com/example/platform/app/NavigationTest.kt b/app/src/androidTest/java/com/example/platform/app/NavigationTest.kt new file mode 100644 index 00000000..8c816c46 --- /dev/null +++ b/app/src/androidTest/java/com/example/platform/app/NavigationTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.app + +import android.Manifest +import android.os.Build +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasScrollAction +import androidx.compose.ui.test.hasScrollToIndexAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToKey +import androidx.test.espresso.Espresso +import androidx.test.rule.GrantPermissionRule +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Rule +import org.junit.Test + +/** + * Tests all the samples are present and open. + * + * Note: consider changing the test to use the TestNavController to control navigation instead + */ +@HiltAndroidTest +class NavigationTest { + + /** + * Manages the components' state and is used to perform injection on your test + */ + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + /** + * Use the primary activity to initialize the app normally. + */ + @get:Rule(order = 2) + val composeTestRule = createAndroidComposeRule() + + /** + * Avoids showing permission dialog when running certain samples + */ + @get:Rule(order = 3) + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.CAMERA, + ) + + @Test + fun testSamplesOpen() { + composeTestRule.apply { + val platformLabel = onNodeWithText(activity.getString(R.string.app_name)) + val scrollNode = onAllNodes(hasScrollAction() and hasScrollToIndexAction()).onFirst() + + // For each sample find it in the list, open it and go back + activity.catalogSamples.forEach { + // Skip disabled samples + if (Build.VERSION.SDK_INT >= it.minSDK) { + try { + scrollNode.performScrollToKey(it.route) + onNode(hasText(it.name) and hasText(it.description)).performClick() + + // Go back + Espresso.pressBack() + platformLabel.assertIsDisplayed() + } catch (e: Exception) { + throw Exception("Test failed in sample ${it.name}", e) + } + } + } + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f0886146 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/platform/app/MainActivity.kt b/app/src/main/java/com/example/platform/app/MainActivity.kt new file mode 100644 index 00000000..a84b56d1 --- /dev/null +++ b/app/src/main/java/com/example/platform/app/MainActivity.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.app + +import android.app.Application +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.decode.VideoFrameDecoder +import com.google.android.catalog.framework.ui.CatalogActivity +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.HiltAndroidApp + +/** + * Main app for the platform samples catalog necessary for injecting the list of available samples + * + * Check [casa-android](https://github.com/google/casa-android#create-catalog-app) setup + */ +@HiltAndroidApp +class MainApp : Application(), ImageLoaderFactory { + + override fun newImageLoader(): ImageLoader { + return ImageLoader.Builder(this) + .components { + add(VideoFrameDecoder.Factory()) + } + .crossfade(true) + .build() + } +} + +/** + * Entry point for the platform samples catalog using the [CatalogActivity]. + */ +@AndroidEntryPoint +class MainActivity : CatalogActivity() diff --git a/app/src/main/res/drawable-v24/ic_launcher_background.xml b/app/src/main/res/drawable-v24/ic_launcher_background.xml new file mode 100644 index 00000000..c1920261 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_background.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..d19c85f5 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_monochrome.xml b/app/src/main/res/drawable-v24/ic_launcher_monochrome.xml new file mode 100644 index 00000000..13e6e814 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_monochrome.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..13906015 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..c3010060 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..7bd5c901 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..426fed3a Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..e5f7b6ec Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..070cba7e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..79d063a4 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + Platform Samples + https://github.com/android/platform-samples/tree/main/samples/%1$s + https://developer.android.com/about + https://github.com/android/platform-samples/issues/new?template=bug_report.md&title=%1$s + \ No newline at end of file diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 00000000..f921d475 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.kotlin.jvm) + `java-gradle-plugin` +} + +group = "com.example.platform.plugin.buildlogic" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation(gradleKotlinDsl()) + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.kotlin.gradlePlugin) +} + +gradlePlugin { + plugins { + register("platform") { + id = "com.example.platform" + implementationClass = "com.example.platform.plugin.PlatformPlugin" + } + register("platformSample") { + id = "com.example.platform.sample" + implementationClass = "com.example.platform.plugin.SamplePlugin" + } + register("commonConvention") { + id = "com.example.platform.convention" + implementationClass = "com.example.platform.plugin.CommonConventionPlugin" + } + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 00000000..4c1857b0 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" \ No newline at end of file diff --git a/build-logic/src/main/kotlin/com.example.platform.plugin/CommonConventionPlugin.kt b/build-logic/src/main/kotlin/com.example.platform.plugin/CommonConventionPlugin.kt new file mode 100644 index 00000000..ccdd8331 --- /dev/null +++ b/build-logic/src/main/kotlin/com.example.platform.plugin/CommonConventionPlugin.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions +import org.gradle.kotlin.dsl.configure + +class CommonConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + tasks.whenTaskAdded { task -> + if (task.name == "connectedDebugAndroidTest") { + task.finalizedBy("uninstallDebugAndroidTest") + } + } + + configurations.configureEach { + it.resolutionStrategy.eachDependency { details -> + // Make sure that we"re using the Android version of Guava + if (details.requested.group == "com.google.guava" + && details.requested.module.name == "guava" + && details.requested.version?.contains("jre") == true) { + details.useVersion(details.requested.version!!.replace("jre", "android")) + } + } + } + + pluginManager.withPlugin("java") { + extensions.configure { + toolchain { + it.languageVersion.set(JavaLanguageVersion.of(17)) + } + } + } + + pluginManager.withPlugin("org.jebrains.kotlin.jvm") { + extensions.configure { + // Treat all Kotlin warnings as errors + allWarningsAsErrors = true + // Set JVM target to 17 + jvmTarget = "17" + // Allow use of @OptIn + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + // Enable default methods in interfaces + freeCompilerArgs += "-Xjvm-default=all" + } + } + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/com.example.platform.plugin/CreateSample.kt b/build-logic/src/main/kotlin/com.example.platform.plugin/CreateSample.kt new file mode 100644 index 00000000..25b06937 --- /dev/null +++ b/build-logic/src/main/kotlin/com.example.platform.plugin/CreateSample.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import java.io.File +import java.time.LocalDate +import java.util.Locale + +/** + * This task is used to create a new sample in the project. Given a path and a name it generates + * the folder structure (if not there) and the sample target (using compose target) based on the + * provided name. + * + * Usage: + * $ ./gradlew createSample --path folder/optional-subfolder --name SampleName + */ +abstract class CreateSample : DefaultTask() { + + var samplePath: String = "" + @Option(option = "path", description = "Defines the path for the sample") + set + @Input + get + + var sampleName: String = "" + @Option(option = "name", description = "Defines the sample module name") + set + @Input + get + + @get:Internal + abstract val projectDir: DirectoryProperty + + @TaskAction + fun create() { + require(samplePath.isNotEmpty()) { + "Missing path, provide the path of the sample" + } + require(sampleName.isNotEmpty()) { + "Missing sample module name" + } + + // remove samples prefix if added in the path + samplePath = samplePath.removePrefix("samples/") + + val projectDirFile = projectDir.asFile.get() + val directory = File(projectDirFile, "samples/$samplePath") + + println("Creating $sampleName in $directory") + + val packagePath = samplePath.replace("/", ".").replace("-", ".") + val samplePackage = "com.example.platform.${packagePath}" + + // Create module structure if it doesn't exists. + if (!File(directory, "build.gradle.kts").exists()) { + println("Creating build.gradle.kts for module") + File(directory, "build.gradle.kts").apply { + parentFile.mkdirs() + createNewFile() + writeText(sampleBuildTemplate(samplePackage)) + } + } + + if (!File(directory, "README.md").exists()) { + println("Creating README for module") + File(directory, "README.md").apply { + parentFile.mkdirs() + createNewFile() + writeText(readmeTemplate(sampleName)) + } + } + + // Create sample target file using Compose target + val sampleName = sampleName.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + val samplePath = "src/main/java/${samplePackage.replace(".", "/")}/$sampleName.kt" + File(directory, samplePath).apply { + parentFile.mkdirs() + createNewFile() + writeText(sampleTemplate(samplePackage, sampleName)) + } + + println("Done! Sync and build project") + } +} + +private val projectLicense = """ +/* + * Copyright ${LocalDate.now().year} The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +""" + +private fun sampleBuildTemplate(samplePackage: String) = """ +$projectLicense + +plugins { + id("com.example.platform.sample") +} + +android { + namespace = "$samplePackage" +} + +dependencies { + // Add samples specific dependencies +} +""".trimIndent() + +private fun sampleTemplate(samplePackage: String, moduleName: String) = """ +$projectLicense + +package $samplePackage + +import androidx.compose.runtime.Composable +import com.google.android.catalog.framework.annotations.Sample + +@Sample( + name = "$moduleName", + description = "TODO: Add description" +) +@Composable +fun $moduleName() { + // TODO: implement your sample. + // You can also use Activity or Fragment, simply tag them with the @Sample annotation +} +""".trimIndent() + +private fun readmeTemplate(sampleName: String) = """ +# $sampleName samples + +// TODO: provide minimal instructions +``` +""".trimIndent() \ No newline at end of file diff --git a/build-logic/src/main/kotlin/com.example.platform.plugin/PlatformPlugin.kt b/build-logic/src/main/kotlin/com.example.platform.plugin/PlatformPlugin.kt new file mode 100644 index 00000000..6154a97e --- /dev/null +++ b/build-logic/src/main/kotlin/com.example.platform.plugin/PlatformPlugin.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class PlatformPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + tasks.register("createSample", CreateSample::class.java) { + it.projectDir.set(project.projectDir) + } + } + } +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/com.example.platform.plugin/SamplePlugin.kt b/build-logic/src/main/kotlin/com.example.platform.plugin/SamplePlugin.kt new file mode 100644 index 00000000..1e5828dd --- /dev/null +++ b/build-logic/src/main/kotlin/com.example.platform.plugin/SamplePlugin.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.plugin + +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.project +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +@Suppress("UnstableApiUsage") +class SamplePlugin : Plugin { + override fun apply(target: Project) { + with(target) { + + val libs = extensions + .getByType(VersionCatalogsExtension::class.java) + .named("libs") + + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + apply("com.google.devtools.ksp") + apply("dagger.hilt.android.plugin") + apply("kotlin-parcelize") + apply() + } + + pluginManager.withPlugin("java") { + extensions.configure { + toolchain { + it.languageVersion.set(JavaLanguageVersion.of(17)) + } + } + } + + // TODO: remove when KSP starts respecting the Java/Kotlin toolchain + tasks.withType(KotlinCompile::class.java).configureEach { + it.kotlinOptions { + jvmTarget = "17" + } + } + + pluginManager.withPlugin("com.android.library") { + configure { + compileSdk = 34 + defaultConfig { + minSdk = 21 + @Suppress("DEPRECATION") + targetSdk = 34 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = + libs.findVersion("composeCompiler").get().toString() + } + } + } + + dependencies { + // Do not add the shared module to itself + if (!project.displayName.contains("samples:base")) { + "implementation"(project(":samples:base")) + } + + "implementation"(platform(libs.findLibrary("compose.bom").get())) + "androidTestImplementation"(platform(libs.findLibrary("compose.bom").get())) + + "implementation"(libs.findLibrary("casa.base").get()) + "implementation"(libs.findLibrary("casa.ui").get()) + "ksp"(libs.findLibrary("casa.processor").get()) + + "implementation"(libs.findLibrary("hilt.android").get()) + "ksp"(libs.findLibrary("hilt.compiler").get()) + + "implementation"(libs.findLibrary("androidx.core").get()) + "implementation"(libs.findLibrary("androidx.fragment").get()) + "implementation"(libs.findLibrary("androidx.activity.compose").get()) + "implementation"(libs.findLibrary("compose.foundation.foundation").get()) + "implementation"(libs.findLibrary("compose.runtime.runtime").get()) + "implementation"(libs.findLibrary("compose.runtime.livedata").get()) + "implementation"(libs.findLibrary("androidx.lifecycle.viewmodel.compose").get()) + "implementation"(libs.findLibrary("compose.ui.ui").get()) + "implementation"(libs.findLibrary("compose.material3").get()) + "implementation"(libs.findLibrary("compose.material.iconsext").get()) + + + "implementation"(libs.findLibrary("coil.compose").get()) + "implementation"(libs.findLibrary("coil.video").get()) + + "implementation"(libs.findLibrary("accompanist.permissions").get()) + + "implementation"(libs.findLibrary("compose.ui.tooling.preview").get()) + "debugImplementation"(libs.findLibrary("compose.ui.tooling").get()) + + "androidTestImplementation"(libs.findLibrary("androidx.test.core").get()) + "androidTestImplementation"(libs.findLibrary("androidx.test.runner").get()) + } + } + } +} diff --git a/build-logic/src/main/kotlin/com.example.platform.plugin/SyncSamplesInfo.kt b/build-logic/src/main/kotlin/com.example.platform.plugin/SyncSamplesInfo.kt new file mode 100644 index 00000000..10e3bfe5 --- /dev/null +++ b/build-logic/src/main/kotlin/com.example.platform.plugin/SyncSamplesInfo.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class SyncSamplesInfo : DefaultTask() { + + @get:Internal + abstract val projectDir: DirectoryProperty + + @TaskAction + fun create() { + val projectDirFile = projectDir.asFile.get() + val samplesFolder = File(projectDirFile, "samples") + val samples = mutableListOf() + samplesFolder.walkBottomUp().forEach { file -> + if (file.extension == "kt") { + val text = file.readText() + if (text.contains("@Sample")) { + val sample = SampleInfo( + name = text.findTag("name = "), + description = text.findTag("description = "), + path = file.path.removePrefix(samplesFolder.path + "/"), + ) + samples.add(sample) + } + } + } + + // Delete previously created file in case name changed + File(projectDirFile, ".idea/runConfigurations").walkBottomUp().forEach { file -> + if (file.isFile && file.name.startsWith("Sample-")) { + file.delete() + } + } + samples.forEach { sample -> + createRunConfig(projectDirFile, sample.name) + } + + createSamplesList(projectDirFile, samples) + } + + private fun String.findTag(tag: String): String { + val index = indexOf(tag) + return if (index >= 0) { + val startIndex = index + tag.length + 1 + substring(startIndex until indexOf("\"", startIndex)) + } else { + "" + } + } +} + +private data class SampleInfo(val name: String, val description: String, val path: String) + +private fun createSamplesList(projectDir: File, samples: List) { + val readme = buildString { + append("# Available Samples\n\n") + + samples.sortedBy { it.name }.forEach { + append("- [${it.name}](${it.path}):\n${it.description}\n") + } + } + File(projectDir, "samples/README.md").apply { + createNewFile() + writeText(readme) + } +} + +private fun createRunConfig(projectDir: File, sampleName: String) { + val ideaDir = File(projectDir, ".idea/runConfigurations") + val startCommand = + "