├── .allstar └── binary_artifacts.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── auto-merge.yml ├── ci-gradle.properties ├── pull_request_template.md ├── release-drafter.yml └── workflows │ ├── automerger.yml │ ├── build-snapshot.yml │ ├── build.yml │ ├── device-tests.yml │ ├── issues-stale.yml │ └── update-release.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── copyright │ ├── AOSP.xml │ └── profiles_settings.xml ├── inspectionProfiles │ ├── ktlint.xml │ └── profiles_settings.xml ├── kotlinScripting.xml ├── kotlinc.xml └── vcs.xml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appwidget-configuration ├── .gitignore ├── README.md ├── api │ └── current.api ├── build.gradle ├── consumer-rules.pro ├── gradle.properties ├── images │ └── glance-configuration-demo.gif ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── google │ └── android │ └── glance │ └── appwidget │ └── configuration │ └── AppWidgetConfigurationScaffold.kt ├── appwidget-host ├── .gitignore ├── README.md ├── api │ └── current.api ├── build.gradle ├── consumer-rules.pro ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── google │ └── android │ └── glance │ └── appwidget │ └── host │ ├── AppWidgetHost.kt │ ├── AppWidgetHostPreview.kt │ ├── AppWidgetHostUtils.kt │ ├── AppWidgetSizeUtils.kt │ └── glance │ └── GlanceAppWidgetHostPreview.kt ├── appwidget-testing ├── .gitignore ├── README.md ├── api │ └── current.api ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── google │ │ └── android │ │ └── glance │ │ └── appwidget │ │ └── testing │ │ └── GlanceScreenshotTestActivity.kt │ └── res │ └── layout │ └── test_activity_layout.xml ├── appwidget-viewer ├── .gitignore ├── README.md ├── api │ └── current.api ├── build.gradle ├── consumer-rules.pro ├── gradle.properties ├── images │ ├── live-edit-showcase.gif │ ├── preview-device-file-explorer.png │ ├── preview-info-panel.png │ ├── preview-layout-inspector.png │ ├── preview-resize-exact.gif │ ├── preview-resize-responsive.gif │ ├── preview-resize-single.gif │ └── preview-widget-selector.png ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── google │ └── android │ └── glance │ └── tools │ └── viewer │ ├── GlanceSnapshot.kt │ ├── GlanceViewerActivity.kt │ └── ui │ ├── ViewerInfoPanel.kt │ ├── ViewerPanel.kt │ ├── ViewerResizePanel.kt │ ├── ViewerScreen.kt │ └── theme │ ├── Color.kt │ ├── Theme.kt │ └── Type.kt ├── build.gradle ├── checksum.sh ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── release ├── secring.gpg.aes ├── signing-cleanup.sh ├── signing-setup.sh └── signing.properties.aes ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── debug │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── google │ │ └── android │ │ └── glance │ │ └── tools │ │ └── sample │ │ └── SampleViewerActivity.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── google │ │ │ └── android │ │ │ └── glance │ │ │ └── tools │ │ │ └── sample │ │ │ ├── AppWidgetConfigurationActivity.kt │ │ │ ├── SampleAppWidgetReceiver.kt │ │ │ └── SampleGlanceWidgetReceiver.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── app_widget_preview.png │ │ ├── glance_widget_preview.png │ │ ├── ic_android.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── widget_loading.xml │ │ └── widget_sample.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ ├── app_widget_sample.xml │ │ └── glance_widget_sample.xml │ └── test │ ├── java │ └── com │ │ └── google │ │ └── android │ │ └── glance │ │ └── tools │ │ └── testing │ │ └── SampleGlanceScreenshotTest.kt │ └── resources │ └── golden │ ├── sample_content.png │ └── sample_content_rtl.png ├── scripts ├── generate_docs.sh └── run-tests.sh ├── settings.gradle └── spotless ├── copyright.txt └── greclipse.properties /.allstar/binary_artifacts.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2022 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Exemption reason: This repo uses binary artifacts to ship gradle.jar for users. It does not allow any others. 18 | # Exemption timeframe: permanent 19 | optConfig: 20 | optOut: true 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Screenshots? 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ## Environment: 27 | - Android OS version: [e.g. Android 5.0] 28 | - Device: [e.g. Emulator, Google Pixel 4] 29 | - Glance Experimental Tools version: [e.g. v0.X] 30 | 31 | ## Additional context 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Config for github.com/bobvanderlinden/probot-auto-merge 2 | minApprovals: 3 | COLLABORATOR: 1 4 | requiredLabels: 5 | - automerge 6 | mergeMethod: merge 7 | reportStatus: true -------------------------------------------------------------------------------- /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Turn Gradle daemon off due to https://github.com/Kotlin/dokka/issues/1405 18 | org.gradle.daemon=false 19 | 20 | org.gradle.parallel=true 21 | org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=1536m -XX:+HeapDumpOnOutOfMemoryError 22 | org.gradle.workers.max=2 23 | 24 | kotlin.compiler.execution.strategy=in-process 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Please add the library name to the PR title. Example: "[Preview] Fixes typo" ### 2 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | template: | 4 | ## What’s Changed 5 | 6 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/automerger.yml: -------------------------------------------------------------------------------- 1 | name: main to snapshot auto merger 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | automerge: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: '0' # 0 == fetch all history history 16 | ref: 'snapshot' 17 | token: ${{ secrets.AUTOMERGE_PAT }} 18 | 19 | - run: | 20 | git config user.name github-actions 21 | git config user.email github-actions@github.com 22 | git fetch origin 23 | git merge origin/main --no-edit 24 | git push 25 | -------------------------------------------------------------------------------- /.github/workflows/build-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Build & test - Snapshots 2 | 3 | on: 4 | push: 5 | branches: 6 | - snapshot 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | # Skip build if head commit contains 'skip ci' 14 | if: "!contains(github.event.head_commit.message, 'skip ci')" 15 | 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 30 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | # Fetch expanded history, which is needed for affected module detection 23 | fetch-depth: '500' 24 | 25 | - name: Copy CI gradle.properties 26 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 27 | 28 | - name: set up JDK 29 | uses: actions/setup-java@v1 30 | with: 31 | java-version: 17 32 | 33 | - name: Decrypt secrets 34 | run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }} 35 | 36 | - name: Generate cache key 37 | run: ./checksum.sh checksum.txt 38 | 39 | - uses: actions/cache@v2 40 | with: 41 | path: | 42 | ~/.gradle/caches/modules-* 43 | ~/.gradle/caches/jars-* 44 | ~/.gradle/caches/build-cache-* 45 | key: gradle-${{ hashFiles('checksum.txt') }} 46 | 47 | - name: Build 48 | run: | 49 | ./gradlew --scan --stacktrace \ 50 | spotlessCheck \ 51 | assemble \ 52 | metalavaCheckCompatibilityDebug \ 53 | lintDebug 54 | 55 | - name: Unit Tests 56 | run: | 57 | ./scripts/run-tests.sh \ 58 | --unit-tests \ 59 | --run-affected \ 60 | --affected-base-ref=$BASE_REF 61 | 62 | - name: Upload test results 63 | if: always() 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: test-results-robolectric 67 | path: | 68 | **/build/test-results/* 69 | **/build/reports/* 70 | 71 | - name: Clean secrets 72 | if: always() 73 | run: release/signing-cleanup.sh 74 | 75 | test: 76 | runs-on: ubuntu-latest 77 | needs: build 78 | timeout-minutes: 50 79 | 80 | strategy: 81 | # Allow tests to continue on other devices if they fail on one device. 82 | fail-fast: false 83 | matrix: 84 | api-level: [ 26, 28, 29 ] 85 | shard: [ 0, 1 ] # Need to update shard-count below if this changes 86 | 87 | env: 88 | TERM: dumb 89 | 90 | steps: 91 | - uses: actions/checkout@v2 92 | with: 93 | # Fetch expanded history, which is needed for affected module detection 94 | fetch-depth: '500' 95 | 96 | - name: Copy CI gradle.properties 97 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 98 | 99 | - name: set up JDK 100 | uses: actions/setup-java@v1 101 | with: 102 | java-version: 17 103 | 104 | - name: Decrypt secrets 105 | run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }} 106 | 107 | - name: Generate cache key 108 | run: ./checksum.sh checksum.txt 109 | 110 | - uses: actions/cache@v2 111 | with: 112 | path: | 113 | ~/.gradle/caches/modules-* 114 | ~/.gradle/caches/jars-* 115 | ~/.gradle/caches/build-cache-* 116 | key: gradle-${{ hashFiles('checksum.txt') }} 117 | 118 | # Determine what emulator image to use. We run all API 28+ emulators using 119 | # the google_apis image 120 | - name: Determine emulator target 121 | id: determine-target 122 | env: 123 | API_LEVEL: ${{ matrix.api-level }} 124 | run: | 125 | TARGET="default" 126 | if [ "$API_LEVEL" -ge "28" ]; then 127 | TARGET="google_apis" 128 | fi 129 | echo "::set-output name=TARGET::$TARGET" 130 | 131 | - name: Enable KVM group perms 132 | run: | 133 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 134 | sudo udevadm control --reload-rules 135 | sudo udevadm trigger --name-match=kvm 136 | 137 | - name: Run tests 138 | uses: reactivecircus/android-emulator-runner@v2 139 | with: 140 | api-level: ${{ matrix.api-level }} 141 | target: ${{ steps.determine-target.outputs.TARGET }} 142 | profile: Galaxy Nexus 143 | script: ./scripts/run-tests.sh --log-file=logcat.txt --run-affected --affected-base-ref=$BASE_REF --shard-index=${{ matrix.shard }} --shard-count=2 144 | 145 | - name: Clean secrets 146 | if: always() 147 | run: release/signing-cleanup.sh 148 | 149 | - name: Upload logs 150 | if: always() 151 | uses: actions/upload-artifact@v4 152 | with: 153 | name: logs-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }} 154 | path: logcat.txt 155 | 156 | - name: Upload test results 157 | if: always() 158 | uses: actions/upload-artifact@v4 159 | with: 160 | name: test-results-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }} 161 | path: | 162 | **/build/reports/* 163 | **/build/outputs/*/connected/* 164 | 165 | deploy: 166 | if: github.event_name == 'push' # only deploy for pushed commits (not PRs) 167 | 168 | runs-on: ubuntu-latest 169 | needs: [ build, test ] 170 | timeout-minutes: 30 171 | env: 172 | TERM: dumb 173 | 174 | steps: 175 | - uses: actions/checkout@v2 176 | 177 | - name: Copy CI gradle.properties 178 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 179 | 180 | - name: set up JDK 181 | uses: actions/setup-java@v1 182 | with: 183 | java-version: 17 184 | 185 | - name: Decrypt secrets 186 | run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }} 187 | 188 | - name: Generate cache key 189 | run: ./checksum.sh checksum.txt 190 | 191 | - uses: actions/cache@v2 192 | with: 193 | path: | 194 | ~/.gradle/caches/modules-* 195 | ~/.gradle/caches/jars-* 196 | ~/.gradle/caches/build-cache-* 197 | key: gradle-${{ hashFiles('checksum.txt') }} 198 | 199 | - name: Deploy to Sonatype 200 | run: ./gradlew publish --no-parallel --stacktrace 201 | env: 202 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 203 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 204 | 205 | - name: Clean secrets 206 | if: always() 207 | run: release/signing-cleanup.sh 208 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | # Skip build if head commit contains 'skip ci' 14 | if: "!contains(github.event.head_commit.message, 'skip ci')" 15 | 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 30 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | # Fetch expanded history, which is needed for affected module detection 23 | fetch-depth: '500' 24 | 25 | - name: Copy CI gradle.properties 26 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 27 | 28 | - name: set up JDK 29 | uses: actions/setup-java@v1 30 | with: 31 | java-version: 17 32 | 33 | - name: Decrypt secrets 34 | run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }} 35 | 36 | - name: Generate cache key 37 | run: ./checksum.sh checksum.txt 38 | 39 | - uses: actions/cache@v2 40 | with: 41 | path: | 42 | ~/.gradle/caches/modules-* 43 | ~/.gradle/caches/jars-* 44 | ~/.gradle/caches/build-cache-* 45 | key: gradle-${{ hashFiles('checksum.txt') }} 46 | 47 | - name: Build 48 | run: | 49 | ./gradlew --scan --stacktrace \ 50 | spotlessCheck \ 51 | assemble \ 52 | metalavaCheckCompatibilityDebug \ 53 | lintDebug 54 | 55 | - name: Unit Tests 56 | run: | 57 | ./scripts/run-tests.sh \ 58 | --unit-tests \ 59 | --run-affected \ 60 | --affected-base-ref=$BASE_REF 61 | 62 | - name: Upload test results 63 | if: always() 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: test-results-robolectric 67 | path: | 68 | **/build/test-results/* 69 | **/build/reports/* 70 | 71 | - name: Clean secrets 72 | if: always() 73 | run: release/signing-cleanup.sh 74 | 75 | test: 76 | runs-on: ubuntu-latest 77 | needs: build 78 | timeout-minutes: 50 79 | 80 | strategy: 81 | # Allow tests to continue on other devices if they fail on one device. 82 | fail-fast: false 83 | matrix: 84 | api-level: [ 22, 26, 28, 29 ] 85 | shard: [ 0, 1 ] # Need to update shard-count below if this changes 86 | 87 | env: 88 | TERM: dumb 89 | 90 | steps: 91 | - uses: actions/checkout@v2 92 | with: 93 | # Fetch expanded history, which is needed for affected module detection 94 | fetch-depth: '500' 95 | 96 | - name: Copy CI gradle.properties 97 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 98 | 99 | - name: set up JDK 100 | uses: actions/setup-java@v1 101 | with: 102 | java-version: 17 103 | 104 | - name: Decrypt secrets 105 | run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }} 106 | 107 | - name: Generate cache key 108 | run: ./checksum.sh checksum.txt 109 | 110 | - uses: actions/cache@v2 111 | with: 112 | path: | 113 | ~/.gradle/caches/modules-* 114 | ~/.gradle/caches/jars-* 115 | ~/.gradle/caches/build-cache-* 116 | key: gradle-${{ hashFiles('checksum.txt') }} 117 | 118 | # Determine what emulator image to use. We run all API 28+ emulators using 119 | # the google_apis image 120 | - name: Determine emulator target 121 | id: determine-target 122 | env: 123 | API_LEVEL: ${{ matrix.api-level }} 124 | run: | 125 | TARGET="default" 126 | if [ "$API_LEVEL" -ge "28" ]; then 127 | TARGET="google_apis" 128 | fi 129 | echo "::set-output name=TARGET::$TARGET" 130 | 131 | - name: Enable KVM group perms 132 | run: | 133 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 134 | sudo udevadm control --reload-rules 135 | sudo udevadm trigger --name-match=kvm 136 | 137 | 138 | - name: Run tests 139 | uses: reactivecircus/android-emulator-runner@v2 140 | with: 141 | api-level: ${{ matrix.api-level }} 142 | target: ${{ steps.determine-target.outputs.TARGET }} 143 | profile: Galaxy Nexus 144 | script: ./scripts/run-tests.sh --log-file=logcat.txt --run-affected --affected-base-ref=$BASE_REF --shard-index=${{ matrix.shard }} --shard-count=2 145 | 146 | - name: Clean secrets 147 | if: always() 148 | run: release/signing-cleanup.sh 149 | 150 | - name: Upload logs 151 | if: always() 152 | uses: actions/upload-artifact@v4 153 | with: 154 | name: logs-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }} 155 | path: logcat.txt 156 | 157 | - name: Upload test results 158 | if: always() 159 | uses: actions/upload-artifact@v4 160 | with: 161 | name: test-results-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }} 162 | path: | 163 | **/build/reports/* 164 | **/build/outputs/*/connected/* 165 | 166 | deploy: 167 | if: github.event_name == 'push' # only deploy for pushed commits (not PRs) 168 | 169 | runs-on: ubuntu-latest 170 | needs: [ build, test ] 171 | timeout-minutes: 30 172 | env: 173 | TERM: dumb 174 | 175 | steps: 176 | - uses: actions/checkout@v2 177 | 178 | - name: Copy CI gradle.properties 179 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 180 | 181 | - name: set up JDK 182 | uses: actions/setup-java@v1 183 | with: 184 | java-version: 17 185 | 186 | - name: Decrypt secrets 187 | run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }} 188 | 189 | - name: Generate cache key 190 | run: ./checksum.sh checksum.txt 191 | 192 | - uses: actions/cache@v2 193 | with: 194 | path: | 195 | ~/.gradle/caches/modules-* 196 | ~/.gradle/caches/jars-* 197 | ~/.gradle/caches/build-cache-* 198 | key: gradle-${{ hashFiles('checksum.txt') }} 199 | 200 | - name: Deploy to Sonatype 201 | run: ./gradlew publish --no-parallel --stacktrace 202 | env: 203 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 204 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 205 | 206 | - name: Clean secrets 207 | if: always() 208 | run: release/signing-cleanup.sh 209 | -------------------------------------------------------------------------------- /.github/workflows/device-tests.yml: -------------------------------------------------------------------------------- 1 | name: Instrumented tests on device 2 | 3 | on: 4 | schedule: 5 | # Run this twice per day, at 6:13 and 16:13 6 | - cron: '13 6,16 * * *' 7 | 8 | # Also run this workflow whenever we update this file 9 | push: 10 | paths: 11 | - '.github/workflows/device-tests.yml' 12 | 13 | jobs: 14 | android-test: 15 | runs-on: macos-latest 16 | if: github.repository == 'google/glance-experimental-tools' 17 | timeout-minutes: 80 18 | 19 | strategy: 20 | # Allow tests to continue on other devices if they fail on one device. 21 | fail-fast: false 22 | matrix: 23 | api-level: [ 26, 28, 29 ] 24 | shard: [ 0, 1, 2 ] # Need to update shard-count below if this changes 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | 29 | - name: Copy CI gradle.properties 30 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 31 | 32 | - name: set up JDK 33 | uses: actions/setup-java@v1 34 | with: 35 | java-version: 17 36 | 37 | - name: Decrypt secrets 38 | run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }} 39 | 40 | - name: Generate cache key 41 | run: ./checksum.sh checksum.txt 42 | 43 | - uses: actions/cache@v2 44 | with: 45 | path: | 46 | ~/.gradle/caches/modules-* 47 | ~/.gradle/caches/jars-* 48 | ~/.gradle/caches/build-cache-* 49 | key: gradle-${{ hashFiles('checksum.txt') }} 50 | 51 | # Determine what emulator image to use. We run all API 29+ emulators using 52 | # the google_apis image 53 | - name: Determine emulator target 54 | id: determine-target 55 | env: 56 | API_LEVEL: ${{ matrix.api-level }} 57 | run: | 58 | TARGET="default" 59 | if [ "$API_LEVEL" -ge "29" ]; then 60 | TARGET="google_apis" 61 | fi 62 | echo "::set-output name=TARGET::$TARGET" 63 | 64 | - name: Run device tests 65 | uses: reactivecircus/android-emulator-runner@v2 66 | with: 67 | api-level: ${{ matrix.api-level }} 68 | target: ${{ steps.determine-target.outputs.TARGET }} 69 | profile: Galaxy Nexus 70 | #emulator-build: 7425822 # https://github.com/ReactiveCircus/android-emulator-runner/issues/160 71 | arch: x86_64 72 | # We run all tests, sharding them over 3 shards 73 | script: ./scripts/run-tests.sh --log-file=logcat.txt --shard-index=${{ matrix.shard }} --shard-count=3 74 | 75 | - name: Clean secrets 76 | if: always() 77 | run: release/signing-cleanup.sh 78 | 79 | - name: Upload logs 80 | if: always() 81 | uses: actions/upload-artifact@v4 82 | with: 83 | name: logs-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }} 84 | path: logcat.txt 85 | 86 | - name: Upload test results 87 | if: always() 88 | uses: actions/upload-artifact@v4 89 | with: 90 | name: test-results-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }} 91 | path: | 92 | **/build/reports/* 93 | **/build/outputs/*/connected/* 94 | -------------------------------------------------------------------------------- /.github/workflows/issues-stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '15 3 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | 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.' 13 | days-before-stale: 30 14 | days-before-close: 5 15 | exempt-all-pr-milestones: true 16 | exempt-issue-labels: 'waiting for info,waiting on dependency' 17 | -------------------------------------------------------------------------------- /.github/workflows/update-release.yml: -------------------------------------------------------------------------------- 1 | name: Update release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_draft_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle 3 | build/ 4 | 5 | captures 6 | 7 | /local.properties 8 | 9 | # IntelliJ .idea folder 10 | .idea/workspace.xml 11 | .idea/libraries 12 | .idea/caches 13 | .idea/navEditor.xml 14 | .idea/tasks.xml 15 | .idea/modules.xml 16 | .idea/compiler.xml 17 | .idea/jarRepositories.xml 18 | .idea/deploymentTargetDropDown.xml 19 | .idea/misc.xml 20 | .idea/androidTestResultsUserPreferences.xml 21 | gradle.xml 22 | *.iml 23 | 24 | # General 25 | .DS_Store 26 | .externalNativeBuild 27 | 28 | # Do not commit plain-text signing info 29 | release/*.properties 30 | release/*.gpg 31 | 32 | # VS Code config 33 | org.eclipse.buildship.core.prefs 34 | .classpath 35 | .project 36 | 37 | # Temporary API docs 38 | docs/api 39 | package-list-coil-base 40 | 41 | # Mkdocs temporary serving folder 42 | docs-gen 43 | site 44 | *.bak 45 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 20 | 21 | 22 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | xmlns:android 31 | 32 | ^$ 33 | 34 | 35 | 36 |
37 |
38 | 39 | 40 | 41 | xmlns:.* 42 | 43 | ^$ 44 | 45 | 46 | BY_NAME 47 | 48 |
49 |
50 | 51 | 52 | 53 | .*:id 54 | 55 | http://schemas.android.com/apk/res/android 56 | 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 | .*:name 65 | 66 | http://schemas.android.com/apk/res/android 67 | 68 | 69 | 70 |
71 |
72 | 73 | 74 | 75 | name 76 | 77 | ^$ 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | style 87 | 88 | ^$ 89 | 90 | 91 | 92 |
93 |
94 | 95 | 96 | 97 | .* 98 | 99 | ^$ 100 | 101 | 102 | BY_NAME 103 | 104 |
105 |
106 | 107 | 108 | 109 | .* 110 | 111 | http://schemas.android.com/apk/res/android 112 | 113 | 114 | ANDROID_ATTRIBUTE_ORDER 115 | 116 |
117 |
118 | 119 | 120 | 121 | .* 122 | 123 | .* 124 | 125 | 126 | BY_NAME 127 | 128 |
129 |
130 |
131 |
132 | 133 | 138 |
139 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/AOSP.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/ktlint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## New Features/Libraries 7 | 8 | Before contributing large new features and/or libraries please start a discussion 9 | with us first via GitHub Issues and check that we can support it. 10 | We are unable to support all new features, even though we wish we could! If we 11 | are unable to support adding your feature, we always encourage you to open source it 12 | in your own repository to help the Compose community grow. 13 | 14 | ## Contributor License Agreement 15 | 16 | Contributions to this project must be accompanied by a Contributor License 17 | Agreement. You (or your employer) retain the copyright to your contribution, 18 | this simply gives us permission to use and redistribute your contributions as 19 | part of the project. Head over to to see 20 | your current agreements on file or to sign a new one. 21 | 22 | You generally only need to submit a CLA once, so if you've already submitted one 23 | (even if it was for a different project), you probably don't need to do it 24 | again. 25 | 26 | ## Code Reviews 27 | 28 | All submissions, including submissions by project members, require review. We 29 | use GitHub pull requests for this purpose. Consult 30 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 31 | information on using pull requests. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Glance Experimental Tools 2 | 3 | > 🚧 Work in-progress: this library is under heavy development, APIs might change frequently 4 | 5 | This project aims to supplement Jetpack Glance with features that are commonly required by 6 | developers 7 | but not yet available. 8 | 9 | It is a labs like environment for Glance tooling. We use it to help fill known gaps in the 10 | framework, 11 | experiment with new APIs and to gather insight into the development experience of developing a 12 | Glance library. 13 | 14 | ## Libraries 15 | 16 | ### 🧬️ [appwidget-host](./appwidget-host) 17 | 18 | A simple composable to display RemoteViews inside your app or in `@Preview`s enabling 19 | "[Live Edits](https://developer.android.com/studio/run#live-edit)" or 20 | "[Apply Changes](https://developer.android.com/studio/run#apply-changes)". 21 | 22 | ### 🖼️ [appwidget-viewer](./appwidget-viewer) 23 | 24 | A debug tool to view and interact with AppWidget snapshots embedded inside the app. 25 | 26 | ### 🧪 [appwidget-testing](./appwidget-testing) 27 | 28 | An activity to host a Glance composable for screenshot testing, without binding the entire 29 | appWidget. 30 | 31 | ### 🛠️🎨 [appwidget-configuration](./appwidget-configuration) 32 | 33 | A Material3 Scaffold implementation for appwidget configuration activities. 34 | 35 | ## Future? 36 | 37 | Any of the features available in this group of libraries may become obsolete in the future, at which 38 | point they will (probably) become deprecated. 39 | 40 | We will aim to provide a migration path (where possible), to whatever supersedes the functionality. 41 | 42 | ## Why a separate repository? 43 | 44 | We want to iterate, explore and experiment with some new APIs and tooling more freely, without 45 | adding overhead to the main API and avoid API commitments. In addition, some of these features might 46 | not be allowed in AndroidX. 47 | 48 | > Note: this repository follows the [Accompanist](https://github.com/google/accompanist) pattern but 49 | > in a much more narrow scope (read more about the idea behind this pattern 50 | > [here](https://medium.com/androiddevelopers/jetpack-compose-accompanist-an-faq-b55117b02712)) 51 | 52 | ## Contributions 53 | 54 | Please contribute! We will gladly review any pull requests. 55 | Make sure to read the [Contributing](CONTRIBUTING.md) page first though. 56 | 57 | ## License 58 | 59 | ``` 60 | Copyright 2020 The Android Open Source Project 61 | 62 | Licensed under the Apache License, Version 2.0 (the "License"); 63 | you may not use this file except in compliance with the License. 64 | You may obtain a copy of the License at 65 | 66 | https://www.apache.org/licenses/LICENSE-2.0 67 | 68 | Unless required by applicable law or agreed to in writing, software 69 | distributed under the License is distributed on an "AS IS" BASIS, 70 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 71 | See the License for the specific language governing permissions and 72 | limitations under the License. 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /appwidget-configuration/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /appwidget-configuration/README.md: -------------------------------------------------------------------------------- 1 | # GlanceAppWidget Configuration composable 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.google.android.glance.tools/appwidget-configuration)](https://search.maven.org/search?q=g:com.google.android.glance.tools) 4 | 5 | A composable that uses Material3 Scaffold to display and handle the appwidget activity configuration 6 | logic for Glance. 7 | 8 | 9 | 10 | ## Setup 11 | 12 | ```groovy 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | implementation "com.google.android.glance.tools:appwidget-configuration:" 19 | } 20 | ``` 21 | 22 | ## Usage 23 | 24 | Follow the 25 | ["Declare the configuration activity"](https://developer.android.com/guide/topics/appwidgets/configuration#declare) 26 | step in the official guidance to create and register the activity with the Material3 theme of your 27 | choice: 28 | 29 | ```kotlin 30 | class MyConfigurationActivity : ComponentActivity() { 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | setContent { 35 | MyMaterial3Theme { 36 | ConfigurationScreen() 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | Then, add the `AppWidgetConfigurationScaffold` composable with your configuration content UI 44 | and provide the configuration state with the `GlanceAppWidget` instance to use for the preview. 45 | 46 | ```kotlin 47 | @Composable 48 | private fun ConfigurationScreen() { 49 | val scope = rememberCoroutineScope() 50 | val configurationState = rememberAppWidgetConfigurationState(SampleGlanceWidget) 51 | 52 | // If we don't have a valid id, discard configuration and finish the activity. 53 | if (configurationState.glanceId == null) { 54 | configurationState.discardConfiguration() 55 | return 56 | } 57 | 58 | AppWidgetConfigurationScaffold( 59 | appWidgetConfigurationState = configurationState, 60 | floatingActionButton = { 61 | FloatingActionButton(onClick = { 62 | scope.launch { 63 | configurationState.applyConfiguration() 64 | } 65 | }) { 66 | Icon(imageVector = Icons.Rounded.Done, contentDescription = "Save changes") 67 | } 68 | } 69 | ) { 70 | // Add your configuration content 71 | } 72 | } 73 | ``` 74 | 75 | Use the `AppWidgetConfigurationState` methods to update the `GlanceAppWidget` state shown in the 76 | preview and to apply or discard the changes. 77 | 78 | * `updateCurrentState(update: (T) -> T)`: updates the `GlanceAppWidget` state for the configuration preview without modifying the actual state. 79 | * `getCurrentState()`: get the current `GlanceAppWidget` state for the configuration preview (before calling `updateCurrentState` it will be the actual state). 80 | * `applyConfiguration()`: updates the actual `GlanceAppWidget` instance state and finish the activity. 81 | * `discardConfiguration()`: discards any state changes and finishes the activity with `RESULT_CANCELED`. 82 | 83 | Check the 84 | [AppWidgetConfigurationActivity](../sample/src/main/java/com/google/android/glance/tools/sample/AppWidgetConfigurationActivity.kt) 85 | sample for more information. 86 | 87 | ## Snapshots 88 | 89 | Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. 90 | These are updated on every commit. 91 | 92 | [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/android/glance/tools/appwidget-host/ -------------------------------------------------------------------------------- /appwidget-configuration/api/current.api: -------------------------------------------------------------------------------- 1 | // Signature format: 4.0 2 | package com.google.android.glance.appwidget.configuration { 3 | 4 | public final class AppWidgetConfigurationScaffoldKt { 5 | method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable @androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi public static void AppWidgetConfigurationScaffold(com.google.android.glance.appwidget.configuration.AppWidgetConfigurationState appWidgetConfigurationState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0 topBar, optional kotlin.jvm.functions.Function0 bottomBar, optional kotlin.jvm.functions.Function0 snackbarHost, optional kotlin.jvm.functions.Function0 floatingActionButton, optional int floatingActionButtonPosition, optional long previewColor, optional long displaySize, optional long containerColor, optional long contentColor, kotlin.jvm.functions.Function1 content); 6 | method @androidx.compose.runtime.Composable public static com.google.android.glance.appwidget.configuration.AppWidgetConfigurationState rememberAppWidgetConfigurationState(androidx.glance.appwidget.GlanceAppWidget configurationInstance); 7 | } 8 | 9 | public final class AppWidgetConfigurationState { 10 | ctor public AppWidgetConfigurationState(Object? state, androidx.glance.GlanceId? glanceId, android.appwidget.AppWidgetProviderInfo? providerInfo, androidx.glance.appwidget.GlanceAppWidget instance, android.app.Activity activity); 11 | method public suspend Object? applyConfiguration(kotlin.coroutines.Continuation); 12 | method public void discardConfiguration(); 13 | method public inline T? getCurrentState(); 14 | method public androidx.glance.GlanceId? getGlanceId(); 15 | method public androidx.glance.appwidget.GlanceAppWidget getInstance(); 16 | method public android.appwidget.AppWidgetProviderInfo? getProviderInfo(); 17 | method public inline void updateCurrentState(kotlin.jvm.functions.Function1 update); 18 | property public final androidx.glance.GlanceId? glanceId; 19 | property public final androidx.glance.appwidget.GlanceAppWidget instance; 20 | property public final android.appwidget.AppWidgetProviderInfo? providerInfo; 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /appwidget-configuration/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | id 'org.jetbrains.dokka' 21 | alias(libs.plugins.compose.compiler) 22 | } 23 | 24 | kotlin { 25 | } 26 | 27 | android { 28 | namespace 'com.google.android.glance.appwidget.configuration' 29 | compileSdk 35 30 | 31 | defaultConfig { 32 | minSdk 21 33 | targetSdk 35 34 | 35 | consumerProguardFiles "consumer-rules.pro" 36 | 37 | vectorDrawables { 38 | useSupportLibrary true 39 | } 40 | 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | // The following argument makes the Android Test Orchestrator run its 43 | // "pm clear" command after each test invocation. This command ensures 44 | // that the app's state is completely cleared between tests. 45 | testInstrumentationRunnerArguments clearPackageData: 'true' 46 | } 47 | 48 | buildTypes { 49 | release { 50 | minifyEnabled false 51 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 52 | } 53 | } 54 | compileOptions { 55 | sourceCompatibility JavaVersion.VERSION_17 56 | targetCompatibility JavaVersion.VERSION_17 57 | } 58 | kotlinOptions { 59 | jvmTarget = '17' 60 | } 61 | buildFeatures { 62 | buildConfig false 63 | compose true 64 | } 65 | 66 | 67 | testOptions { 68 | unitTests { 69 | includeAndroidResources = true 70 | } 71 | animationsDisabled true 72 | execution 'ANDROIDX_TEST_ORCHESTRATOR' 73 | } 74 | packagingOptions { 75 | resources { 76 | excludes += [ 77 | '/META-INF/AL2.0', 78 | '/META-INF/LGPL2.1' 79 | ] 80 | } 81 | } 82 | 83 | 84 | publishing { 85 | multipleVariants { 86 | allVariants() 87 | } 88 | } 89 | lint { 90 | checkReleaseBuilds false 91 | textOutput file('stdout') 92 | textReport true 93 | } 94 | } 95 | 96 | dependencies { 97 | api project(":appwidget-host") 98 | 99 | implementation platform(libs.androidx.compose.bom) 100 | implementation libs.glance.appwidget 101 | implementation libs.compose.foundation.foundation 102 | implementation libs.compose.ui.ui 103 | implementation libs.compose.material.material3 104 | 105 | // ====================== 106 | // Test dependencies 107 | // ====================== 108 | 109 | androidTestUtil libs.androidx.test.orchestrator 110 | 111 | androidTestImplementation libs.junit 112 | androidTestImplementation libs.truth 113 | androidTestImplementation libs.compose.ui.test.junit4 114 | androidTestImplementation libs.compose.ui.test.manifest 115 | androidTestImplementation libs.androidx.test.core 116 | androidTestImplementation libs.androidx.test.runner 117 | androidTestImplementation libs.androidx.test.rules 118 | androidTestImplementation libs.androidx.test.uiAutomator 119 | } 120 | 121 | apply plugin: "com.vanniktech.maven.publish" 122 | apply plugin: "me.tylerbwong.gradle.metalava" 123 | 124 | metalava { 125 | filename = "api/current.api" 126 | reportLintsAsErrors = true 127 | } -------------------------------------------------------------------------------- /appwidget-configuration/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-configuration/consumer-rules.pro -------------------------------------------------------------------------------- /appwidget-configuration/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=appwidget-configuration 2 | POM_NAME=Glance Experimental Tools - Configuration 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /appwidget-configuration/images/glance-configuration-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-configuration/images/glance-configuration-demo.gif -------------------------------------------------------------------------------- /appwidget-configuration/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /appwidget-configuration/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /appwidget-host/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /appwidget-host/README.md: -------------------------------------------------------------------------------- 1 | # AppWidget Host composable 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.google.android.glance.tools/appwidget-host)](https://search.maven.org/search?q=g:com.google.android.glance.tools) 4 | 5 | A simple composable to display RemoteViews inside your app or to create `@Preview`s that together 6 | with Compose and [Live Edits](https://developer.android.com/jetpack/compose/tooling#live-edit) 7 | enables, [in most situations](https://developer.android.com/studio/run#limitations), a real-time 8 | update mechanism, reflecting code changes nearly instantaneously. 9 | 10 | > Currently there is an issue that Live Edit stops working after the first change. It actually 11 | > updates the code but it does not render the update. To workaround you can click on the 12 | > AppWidgetHost container to force the update. 13 | 14 | > **Note:** This library is used by the appwidget-viewer and appwidget-configuration modules and is 15 | > independent from Glance-appwidget but provides extensions when glance-appwidget dependency is 16 | > present in the project 17 | 18 | ## Setup 19 | 20 | ```groovy 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | // or debugImplementation if only used for previews 27 | implementation "com.google.android.glance.tools:appwidget-host:" 28 | } 29 | ``` 30 | 31 | ## Usage 32 | 33 | Add the `AppWidgetHost` inside your UI by providing the available size for the AppWidget and the 34 | `AppWidgetHostState` to interact with the host. 35 | 36 | You can monitor the `isReady` value to then provide the RemoteViews to display in the host. 37 | 38 | ```kotlin 39 | @Composable 40 | fun MyScreen(provider: AppWidgetProviderInfo) { 41 | val previewHostState = rememberAppWidgetHostState(provider) 42 | if (previewHostState.isReady) { 43 | previewHostState.updateAppWidget( 44 | // Provide your RemoteViews 45 | ) 46 | } 47 | AppWidgetHost( 48 | modifier = Modifier.fillMaxSize().padding(16.dp), 49 | widgetSize = DpSize(200.dp, 200.dp), 50 | state = previewHostState 51 | ) 52 | } 53 | ``` 54 | 55 | > **Important:** when using the `AppWidgetHost` inside an activity with AppCompat theme the host won't 56 | > be able to inflate the RemoteViews. This is because appcompat theme will switch the views to be 57 | > appcompat views and those are not supported by RemoteViews. Few options: 58 | > - Use a different activity 59 | > - Remove the theme (or set `android:viewInflaterClass=@null`) 60 | > - Implement your own `AppCompatViewInflater` 61 | 62 | ### Use for Previews 63 | 64 | The `AppWidgetHostPreview` enables [Jetpack Compose Live Previews](https://developer.android.com/jetpack/compose/tooling) 65 | by creating a `@Preview`composable and running it in a device. 66 | 67 | > Note: while the preview will render in Android Studio, the RemoteViews won't. You must always 68 | > deploy them in a device ([guide](https://developer.android.com/jetpack/compose/tooling#preview-deploy)). 69 | 70 | ```kotlin 71 | @Preview 72 | @Composable 73 | fun MyAppWidgetPreview() { 74 | // The size of the widget 75 | val displaySize = DpSize(200.dp, 200.dp) 76 | 77 | AppWidgetHostPreview( 78 | modifier = Modifier.fillMaxSize(), 79 | displaySize = displaySize 80 | ) { context -> 81 | RemoteViews(context.packageName, R.layout.my_widget_layout) 82 | } 83 | } 84 | ``` 85 | 86 | If you use Glance for appwidget instead, the library provides an extension composable to simplify the setup 87 | 88 | ```kotlin 89 | @OptIn(ExperimentalGlanceRemoteViewsApi::class) 90 | @Preview 91 | @Composable 92 | fun MyGlanceWidgetPreview() { 93 | // The size of the widget 94 | val displaySize = DpSize(200.dp, 200.dp) 95 | // Your GlanceAppWidget instance 96 | val instance = SampleGlanceWidget 97 | // Provide a state depending on the GlanceAppWidget state definition 98 | val state = preferencesOf(SampleGlanceWidget.countKey to 2) 99 | 100 | GlanceAppWidgetHostPreview( 101 | modifier = Modifier.fillMaxSize(), 102 | glanceAppWidget = instance, 103 | state = state, 104 | displaySize = displaySize, 105 | ) 106 | } 107 | ``` 108 | 109 | > Important: don't forget to setup the compose-tooling as explained [here](https://developer.android.com/jetpack/compose/tooling) 110 | 111 | ### Utils 112 | 113 | The library also provides a set of common utils when working with AppWidgets and/or Glance: 114 | 115 | - [AppWidgetHostUtils](src/main/java/com/google/android/glance/appwidget/host/AppWidgetHostUtils.kt) 116 | - [AppWidgetSizeUtils](src/main/java/com/google/android/glance/appwidget/host/AppWidgetSizeUtils.kt) 117 | 118 | ## Snapshots 119 | 120 | Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. 121 | These are updated on every commit. 122 | 123 | [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/android/glance/tools/appwidget-host/ 124 | -------------------------------------------------------------------------------- /appwidget-host/api/current.api: -------------------------------------------------------------------------------- 1 | // Signature format: 4.0 2 | package com.google.android.glance.appwidget.host { 3 | 4 | public final class AppWidgetHostKt { 5 | method @androidx.compose.runtime.Composable public static void AppWidgetHost(optional androidx.compose.ui.Modifier modifier, long displaySize, com.google.android.glance.appwidget.host.AppWidgetHostState state, optional androidx.compose.ui.graphics.Color? gridColor); 6 | method @androidx.compose.runtime.Composable public static com.google.android.glance.appwidget.host.AppWidgetHostState rememberAppWidgetHostState(optional android.appwidget.AppWidgetProviderInfo? providerInfo); 7 | } 8 | 9 | public final class AppWidgetHostPreviewKt { 10 | method @androidx.compose.runtime.Composable public static void AppWidgetHostPreview(optional androidx.compose.ui.Modifier modifier, optional long displaySize, optional android.appwidget.AppWidgetProviderInfo? provider, kotlin.jvm.functions.Function2,?> content); 11 | } 12 | 13 | public final class AppWidgetHostState { 14 | ctor public AppWidgetHostState(android.appwidget.AppWidgetProviderInfo? providerInfo, androidx.compose.runtime.MutableState state); 15 | method public android.appwidget.AppWidgetProviderInfo? getProviderInfo(); 16 | method public android.widget.RemoteViews? getSnapshot(); 17 | method public android.appwidget.AppWidgetHostView? getValue(); 18 | method public boolean isReady(); 19 | method public void updateAppWidget(android.widget.RemoteViews remoteViews); 20 | property public final boolean isReady; 21 | property public final android.appwidget.AppWidgetProviderInfo? providerInfo; 22 | property public final android.widget.RemoteViews? snapshot; 23 | property public final android.appwidget.AppWidgetHostView? value; 24 | } 25 | 26 | public final class AppWidgetHostUtilsKt { 27 | method @RequiresApi(android.os.Build.VERSION_CODES.Q) public static suspend Object? exportSnapshot(android.appwidget.AppWidgetHostView, optional String? fileName, optional kotlin.coroutines.Continuation>); 28 | method public static boolean requestPin(com.google.android.glance.appwidget.host.AppWidgetHostState, optional android.content.ComponentName target, optional android.app.PendingIntent? successCallback); 29 | } 30 | 31 | public final class AppWidgetSizeUtilsKt { 32 | method public static float getAppwidgetBackgroundRadius(android.content.Context); 33 | method public static float getAppwidgetBackgroundRadiusPixels(android.content.Context); 34 | method public static long getMaxSize(android.appwidget.AppWidgetProviderInfo, android.content.Context context); 35 | method public static long getMinSize(android.appwidget.AppWidgetProviderInfo, android.content.Context context); 36 | method public static long getSingleSize(android.appwidget.AppWidgetProviderInfo, android.content.Context context); 37 | method public static long getTargetSize(android.appwidget.AppWidgetProviderInfo, android.content.Context context); 38 | method public static int toPixels(float, android.content.Context context); 39 | method public static int toPixels(float, android.util.DisplayMetrics displayMetrics); 40 | method public static android.os.Bundle toSizeExtras(android.appwidget.AppWidgetProviderInfo, android.content.Context context, long availableSize); 41 | method public static android.util.SizeF toSizeF(long); 42 | } 43 | 44 | } 45 | 46 | package com.google.android.glance.appwidget.host.glance { 47 | 48 | public final class GlanceAppWidgetHostPreviewKt { 49 | method @androidx.compose.runtime.Composable @androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi public static void GlanceAppWidgetHostPreview(androidx.glance.appwidget.GlanceAppWidget glanceAppWidget, optional androidx.compose.ui.Modifier modifier, optional Object? state, optional long displaySize, optional android.appwidget.AppWidgetProviderInfo? provider); 50 | } 51 | 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /appwidget-host/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | id 'org.jetbrains.dokka' 21 | alias(libs.plugins.compose.compiler) 22 | } 23 | 24 | kotlin { 25 | } 26 | 27 | android { 28 | namespace 'com.google.android.glance.appwidget.host' 29 | compileSdk 35 30 | 31 | defaultConfig { 32 | minSdk 21 33 | targetSdk 35 34 | 35 | consumerProguardFiles "consumer-rules.pro" 36 | 37 | vectorDrawables { 38 | useSupportLibrary true 39 | } 40 | 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | // The following argument makes the Android Test Orchestrator run its 43 | // "pm clear" command after each test invocation. This command ensures 44 | // that the app's state is completely cleared between tests. 45 | testInstrumentationRunnerArguments clearPackageData: 'true' 46 | } 47 | 48 | buildTypes { 49 | release { 50 | minifyEnabled false 51 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 52 | } 53 | } 54 | compileOptions { 55 | sourceCompatibility JavaVersion.VERSION_17 56 | targetCompatibility JavaVersion.VERSION_17 57 | } 58 | kotlinOptions { 59 | jvmTarget = '17' 60 | } 61 | buildFeatures { 62 | buildConfig false 63 | compose true 64 | } 65 | 66 | testOptions { 67 | unitTests { 68 | includeAndroidResources = true 69 | } 70 | animationsDisabled true 71 | execution 'ANDROIDX_TEST_ORCHESTRATOR' 72 | } 73 | packagingOptions { 74 | resources { 75 | excludes += [ 76 | '/META-INF/AL2.0', 77 | '/META-INF/LGPL2.1' 78 | ] 79 | } 80 | } 81 | 82 | 83 | publishing { 84 | multipleVariants { 85 | allVariants() 86 | } 87 | } 88 | lint { 89 | checkReleaseBuilds false 90 | textOutput file('stdout') 91 | textReport true 92 | } 93 | } 94 | 95 | 96 | dependencies { 97 | implementation platform(libs.androidx.compose.bom) 98 | implementation libs.compose.foundation.foundation 99 | implementation libs.compose.ui.ui 100 | 101 | compileOnly libs.glance.appwidget 102 | 103 | // ====================== 104 | // Test dependencies 105 | // ====================== 106 | 107 | androidTestUtil libs.androidx.test.orchestrator 108 | 109 | androidTestImplementation libs.junit 110 | androidTestImplementation libs.truth 111 | androidTestImplementation libs.compose.ui.test.junit4 112 | androidTestImplementation libs.compose.ui.test.manifest 113 | androidTestImplementation libs.androidx.test.core 114 | androidTestImplementation libs.androidx.test.runner 115 | androidTestImplementation libs.androidx.test.rules 116 | androidTestImplementation libs.androidx.test.uiAutomator 117 | } 118 | 119 | apply plugin: "com.vanniktech.maven.publish" 120 | apply plugin: "me.tylerbwong.gradle.metalava" 121 | 122 | metalava { 123 | filename = "api/current.api" 124 | reportLintsAsErrors = true 125 | } 126 | -------------------------------------------------------------------------------- /appwidget-host/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-host/consumer-rules.pro -------------------------------------------------------------------------------- /appwidget-host/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=appwidget-host 2 | POM_NAME=Glance Experimental Tools - Host 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /appwidget-host/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /appwidget-host/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /appwidget-host/src/main/java/com/google/android/glance/appwidget/host/AppWidgetHostPreview.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.appwidget.host 18 | 19 | import android.appwidget.AppWidgetProviderInfo 20 | import android.content.Context 21 | import android.widget.RemoteViews 22 | import androidx.compose.foundation.clickable 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.LaunchedEffect 25 | import androidx.compose.runtime.rememberCoroutineScope 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.platform.LocalContext 28 | import androidx.compose.ui.unit.DpSize 29 | import kotlinx.coroutines.launch 30 | 31 | /** 32 | * Use this composable inside a [androidx.compose.ui.tooling.preview.Preview] composable to 33 | * display previews of RemoteViews (e.g using GlanceRemoteViews from Glance-appwidget) 34 | * 35 | * Tip: Click on the container to force a content update. 36 | * 37 | * @param modifier defines the container box for the host 38 | * @param displaySize the available size for the RemoteViews, if not provider it will match parent 39 | * @param provider optionally provide the [AppWidgetProviderInfo] to provide additional information 40 | * for the host. 41 | * @param content a suspend lambda returning the actual RemoteViews 42 | */ 43 | @Composable 44 | fun AppWidgetHostPreview( 45 | modifier: Modifier = Modifier, 46 | displaySize: DpSize = DpSize.Unspecified, 47 | provider: AppWidgetProviderInfo? = null, 48 | content: suspend (Context) -> RemoteViews 49 | ) { 50 | val hostState = rememberAppWidgetHostState(provider) 51 | val scope = rememberCoroutineScope() 52 | val context = LocalContext.current 53 | 54 | suspend fun updateContent() { 55 | hostState.updateAppWidget(content(context)) 56 | } 57 | 58 | if (hostState.isReady) { 59 | LaunchedEffect(hostState.value) { 60 | updateContent() 61 | } 62 | } 63 | AppWidgetHost( 64 | modifier = Modifier 65 | .clickable { 66 | scope.launch { 67 | updateContent() 68 | } 69 | } 70 | .then(modifier), 71 | displaySize = displaySize, 72 | state = hostState 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /appwidget-host/src/main/java/com/google/android/glance/appwidget/host/AppWidgetHostUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.appwidget.host 18 | 19 | import android.app.PendingIntent 20 | import android.appwidget.AppWidgetHostView 21 | import android.appwidget.AppWidgetManager 22 | import android.content.ComponentName 23 | import android.content.ContentValues 24 | import android.graphics.Bitmap 25 | import android.graphics.Canvas 26 | import android.graphics.Path 27 | import android.graphics.RectF 28 | import android.net.Uri 29 | import android.os.Build 30 | import android.os.Bundle 31 | import android.os.Environment 32 | import android.provider.MediaStore 33 | import android.util.Log 34 | import android.view.View 35 | import androidx.annotation.RequiresApi 36 | import kotlinx.coroutines.Dispatchers 37 | import kotlinx.coroutines.withContext 38 | import java.io.File 39 | 40 | private const val SNAPSHOTS_FOLDER = "appwidget-snapshots" 41 | 42 | /** 43 | * Request the launcher to pin the current hosted appwidget if any. 44 | * 45 | * @param target the provider component name or null to use the one defined in the host 46 | * @param successCallback a PendingIntent to send when the launcher successfully placed the widget 47 | * @return true if request was successful, false otherwise 48 | * 49 | * @see AppWidgetManager.requestPinAppWidget 50 | */ 51 | fun AppWidgetHostState.requestPin( 52 | target: ComponentName = value!!.appWidgetInfo.provider, 53 | successCallback: PendingIntent? = null 54 | ): Boolean { 55 | val widgetManager = AppWidgetManager.getInstance(value!!.context) 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && widgetManager.isRequestPinAppWidgetSupported) { 57 | val previewBundle = Bundle().apply { 58 | if (snapshot != null) { 59 | putParcelable(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW, snapshot) 60 | } 61 | } 62 | return widgetManager.requestPinAppWidget(target, previewBundle, successCallback) 63 | } 64 | return false 65 | } 66 | 67 | /** 68 | * Extracts and image from the current host and stores it in the device picture directory 69 | * 70 | * @param fileName optional filename (without extension), otherwise the app name + date will be used. 71 | * @return the result of the operation with the image URI if successful 72 | */ 73 | @RequiresApi(Build.VERSION_CODES.Q) 74 | suspend fun AppWidgetHostView.exportSnapshot(fileName: String? = null): Result { 75 | return runCatching { 76 | withContext(Dispatchers.IO) { 77 | val bitmap = (this@exportSnapshot as View).toBitmap() 78 | val collection = MediaStore.Images.Media.getContentUri( 79 | MediaStore.VOLUME_EXTERNAL_PRIMARY 80 | ) 81 | val dirDest = File(Environment.DIRECTORY_PICTURES, SNAPSHOTS_FOLDER) 82 | val date = System.currentTimeMillis() 83 | val name = fileName ?: getSnapshotName(date) 84 | val newImage = ContentValues().apply { 85 | put(MediaStore.Images.Media.DISPLAY_NAME, "$name.png") 86 | put(MediaStore.MediaColumns.MIME_TYPE, "image/png") 87 | put(MediaStore.MediaColumns.DATE_ADDED, date) 88 | put(MediaStore.MediaColumns.DATE_MODIFIED, date) 89 | put(MediaStore.MediaColumns.SIZE, bitmap.byteCount) 90 | put(MediaStore.MediaColumns.WIDTH, bitmap.width) 91 | put(MediaStore.MediaColumns.HEIGHT, bitmap.height) 92 | put(MediaStore.MediaColumns.RELATIVE_PATH, "$dirDest${File.separator}") 93 | put(MediaStore.Images.Media.IS_PENDING, 1) 94 | } 95 | context.contentResolver.insert(collection, newImage)!!.apply { 96 | context.contentResolver.openOutputStream(this, "w").use { 97 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, it!!) 98 | } 99 | newImage.clear() 100 | newImage.put(MediaStore.Images.Media.IS_PENDING, 0) 101 | context.contentResolver.update(this, newImage, null, null) 102 | } 103 | } 104 | } 105 | } 106 | 107 | private fun AppWidgetHostView.getSnapshotName(date: Long) = ( 108 | try { 109 | appWidgetInfo.loadLabel(context.packageManager) 110 | } catch (e: Exception) { 111 | Log.w("AppWidgetHostView", "Could not retrieve app label", e) 112 | null 113 | } ?: "unknown" 114 | ) + "-$date" 115 | 116 | private fun View.toBitmap(): Bitmap { 117 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 118 | val canvas = Canvas(bitmap) 119 | layout(left, top, right, bottom) 120 | val clipPath = Path() 121 | val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) 122 | val radi = context.appwidgetBackgroundRadiusPixels 123 | clipPath.addRoundRect(rect, radi, radi, Path.Direction.CW) 124 | canvas.clipPath(clipPath) 125 | draw(canvas) 126 | return bitmap 127 | } 128 | -------------------------------------------------------------------------------- /appwidget-host/src/main/java/com/google/android/glance/appwidget/host/AppWidgetSizeUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.appwidget.host 18 | 19 | import android.appwidget.AppWidgetManager 20 | import android.appwidget.AppWidgetProviderInfo 21 | import android.content.Context 22 | import android.os.Build 23 | import android.os.Bundle 24 | import android.util.DisplayMetrics 25 | import android.util.SizeF 26 | import android.util.TypedValue 27 | import androidx.compose.ui.unit.Dp 28 | import androidx.compose.ui.unit.DpSize 29 | import androidx.compose.ui.unit.dp 30 | import kotlin.math.min 31 | 32 | fun DpSize.toSizeF(): SizeF = SizeF(width.value, height.value) 33 | 34 | fun Dp.toPixels(context: Context) = toPixels(context.resources.displayMetrics) 35 | 36 | fun Dp.toPixels(displayMetrics: DisplayMetrics) = 37 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics).toInt() 38 | 39 | internal fun Int.pixelsToDp(context: Context) = pixelsToDp(context.resources.displayMetrics) 40 | 41 | internal fun Int.pixelsToDp(displayMetrics: DisplayMetrics) = (this / displayMetrics.density).dp 42 | 43 | fun AppWidgetProviderInfo.getTargetSize(context: Context): DpSize = DpSize( 44 | minWidth.pixelsToDp(context), 45 | minHeight.pixelsToDp(context) 46 | ) 47 | 48 | fun AppWidgetProviderInfo.getMaxSize(context: Context): DpSize = 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && maxResizeWidth > 0) { 50 | DpSize( 51 | maxResizeWidth.pixelsToDp(context), 52 | maxResizeHeight.pixelsToDp(context) 53 | ) 54 | } else { 55 | DpSize(Int.MAX_VALUE.dp, Int.MAX_VALUE.dp) 56 | } 57 | 58 | fun AppWidgetProviderInfo.getMinSize(context: Context): DpSize = DpSize( 59 | width = minResizeWidth.pixelsToDp(context), 60 | height = minResizeHeight.pixelsToDp(context) 61 | ) 62 | 63 | fun AppWidgetProviderInfo.getSingleSize(context: Context): DpSize { 64 | val minWidth = min( 65 | minWidth, 66 | if (resizeMode and AppWidgetProviderInfo.RESIZE_HORIZONTAL != 0) { 67 | minResizeWidth 68 | } else { 69 | Int.MAX_VALUE 70 | } 71 | ) 72 | val minHeight = min( 73 | minHeight, 74 | if (resizeMode and AppWidgetProviderInfo.RESIZE_VERTICAL != 0) { 75 | minResizeHeight 76 | } else { 77 | Int.MAX_VALUE 78 | } 79 | ) 80 | return DpSize( 81 | minWidth.pixelsToDp(context), 82 | minHeight.pixelsToDp(context) 83 | ) 84 | } 85 | 86 | val Context.appwidgetBackgroundRadius: Dp 87 | get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 88 | val size = resources.getDimensionPixelSize( 89 | android.R.dimen.system_app_widget_background_radius 90 | ) 91 | (size / resources.displayMetrics.density).dp 92 | } else { 93 | 16.dp 94 | } 95 | 96 | val Context.appwidgetBackgroundRadiusPixels: Float 97 | get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 98 | resources.getDimensionPixelSize( 99 | android.R.dimen.system_app_widget_background_radius 100 | ).toFloat() 101 | } else { 102 | (16 * resources.displayMetrics.density) 103 | } 104 | 105 | fun AppWidgetProviderInfo.toSizeExtras(context: Context, availableSize: DpSize): Bundle { 106 | return Bundle().apply { 107 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 108 | putInt( 109 | AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 110 | minResizeWidth 111 | ) 112 | putInt( 113 | AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 114 | minResizeHeight 115 | ) 116 | putInt( 117 | AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 118 | maxResizeWidth 119 | ) 120 | putInt( 121 | AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 122 | maxResizeHeight 123 | ) 124 | putParcelableArrayList( 125 | AppWidgetManager.OPTION_APPWIDGET_SIZES, 126 | arrayListOf(availableSize.toSizeF()) 127 | ) 128 | } else { 129 | // TODO to check how this affects the different glance SizeModes 130 | putInt( 131 | AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 132 | availableSize.width.toPixels(context) 133 | ) 134 | putInt( 135 | AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 136 | availableSize.height.toPixels(context) 137 | ) 138 | putInt( 139 | AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 140 | availableSize.width.toPixels(context) 141 | ) 142 | putInt( 143 | AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 144 | availableSize.height.toPixels(context) 145 | ) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /appwidget-host/src/main/java/com/google/android/glance/appwidget/host/glance/GlanceAppWidgetHostPreview.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.appwidget.host.glance 18 | 19 | import android.appwidget.AppWidgetProviderInfo 20 | import androidx.compose.foundation.clickable 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.LaunchedEffect 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.platform.LocalContext 26 | import androidx.compose.ui.unit.DpSize 27 | import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi 28 | import androidx.glance.appwidget.GlanceAppWidget 29 | import androidx.glance.appwidget.compose 30 | import com.google.android.glance.appwidget.host.AppWidgetHost 31 | import com.google.android.glance.appwidget.host.rememberAppWidgetHostState 32 | import kotlinx.coroutines.launch 33 | 34 | /** 35 | * Use this composable inside a [androidx.compose.ui.tooling.preview.Preview] composable to 36 | * display a glanceable composable 37 | * 38 | * Tip: Click on the container to force a content update. 39 | * 40 | * @param modifier defines the container box for the host 41 | * @param state the state associated to the composable as per [GlanceAppWidget.stateDefinition] 42 | * @param displaySize the available size for the RemoteViews, if not provider it will match parent 43 | * @param provider optionally provide the [AppWidgetProviderInfo] to provide additional information 44 | * for the host. 45 | * @param content a suspend lambda returning the actual RemoteViews 46 | */ 47 | @ExperimentalGlanceRemoteViewsApi 48 | @Composable 49 | fun GlanceAppWidgetHostPreview( 50 | glanceAppWidget: GlanceAppWidget, 51 | modifier: Modifier = Modifier, 52 | state: Any? = null, 53 | displaySize: DpSize = DpSize.Unspecified, 54 | provider: AppWidgetProviderInfo? = null 55 | ) { 56 | val hostState = rememberAppWidgetHostState(provider) 57 | val scope = rememberCoroutineScope() 58 | val context = LocalContext.current 59 | 60 | suspend fun updateContent() { 61 | hostState.updateAppWidget( 62 | glanceAppWidget.compose(context = context, size = displaySize, state = state) 63 | ) 64 | } 65 | 66 | if (hostState.isReady) { 67 | LaunchedEffect(hostState.value) { 68 | updateContent() 69 | } 70 | } 71 | 72 | AppWidgetHost( 73 | modifier = Modifier.clickable { 74 | scope.launch { 75 | updateContent() 76 | } 77 | }.then(modifier), 78 | displaySize = displaySize, 79 | state = hostState 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /appwidget-testing/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | stdout -------------------------------------------------------------------------------- /appwidget-testing/README.md: -------------------------------------------------------------------------------- 1 | # Glance AppWidget Testing 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.google.android.glance.tools/appwidget-testing)](https://search.maven.org/search?q=g:com.google.android.glance.tools) 4 | 5 | ## GlanceScreenshotTestActivity 6 | 7 | A simple activity to render a Glance composable without binding an appwidget for screenshot testing. 8 | It provides functions that can be called to initialize and render the Glance composable in it. 9 | 10 | ### Setup 11 | 12 | ```groovy 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | debugImplementation "com.google.android.glance.tools:appwidget-testing:" 19 | } 20 | ``` 21 | 22 | ### Usage 23 | 24 | Define an `ActivityScenarioRule` in your test class for the `GlanceScreenshotTestActivity`. 25 | 26 | ```kotlin 27 | @get:Rule 28 | val activityScenarioRule = 29 | ActivityScenarioRule(GlanceScreenshotTestActivity::class.java) 30 | ``` 31 | 32 | Initialize the size and state for your composable in the `onActivity` runner and provide the 33 | composable to be rendered in the `GlanceScreenshotTestActivity`. 34 | 35 | ```kotlin 36 | activityScenarioRule.scenario.onActivity { 37 | it.setAppWidgetSize(size) 38 | it.setState(preferencesOf(myKey to 2)) 39 | 40 | it.renderComposable { 41 | MyGlanceContent() 42 | } 43 | } 44 | ``` 45 | 46 | Then, using screenshot testing tool of your choice capture and compare the screenshot of the 47 | activity. For example, following sample uses [Roborazzi](https://github.com/takahirom/roborazzi) 48 | capture and verify the screenshot. 49 | 50 | NOTE: The device and screenshot framework you use should support hardware acceleration and 51 | `clipToOutline` to see rounded corners. For robolectric, see this 52 | [issue](https://github.com/robolectric/robolectric/issues/8081#issuecomment-1478137890). When using 53 | an emulator, you may use Espresso's `captureToBitmap` to ensure that the corner radius is captured. 54 | 55 | ```kotlin 56 | Espresso.onView(ViewMatchers.isRoot()) 57 | .captureRoboImage( 58 | filePath = "src/test/resources/golden/$goldenFileName.png", 59 | roborazziOptions = RoborazziOptions( 60 | compareOptions = RoborazziOptions.CompareOptions( 61 | changeThreshold = 0F 62 | ) 63 | ) 64 | ) 65 | ``` 66 | 67 | For a complete example, see 68 | [SampleGlanceScreenshotTest](https://github.com/google/glance-experimental-tools/tree/main/sample/src/test/java/com/google/android/glance/tools/testing/SampleGlanceScreenshotTest.kt). 69 | -------------------------------------------------------------------------------- /appwidget-testing/api/current.api: -------------------------------------------------------------------------------- 1 | // Signature format: 4.0 2 | package com.google.android.glance.appwidget.testing { 3 | 4 | @RequiresApi(android.os.Build.VERSION_CODES.O) public final class GlanceScreenshotTestActivity extends android.app.Activity { 5 | ctor public GlanceScreenshotTestActivity(); 6 | method public void renderComposable(kotlin.jvm.functions.Function0 composable); 7 | method public void setAppWidgetSize(long size); 8 | method public void setState(T? state); 9 | method public void wrapContentSize(); 10 | } 11 | 12 | } 13 | 14 | -------------------------------------------------------------------------------- /appwidget-testing/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | id 'org.jetbrains.dokka' 21 | alias(libs.plugins.compose.compiler) 22 | } 23 | 24 | kotlin { 25 | explicitApi() 26 | } 27 | 28 | android { 29 | namespace 'com.google.android.glance.appwidget.testing' 30 | compileSdk 35 31 | 32 | defaultConfig { 33 | minSdk 26 34 | targetSdk 35 35 | 36 | consumerProguardFiles "consumer-rules.pro" 37 | 38 | vectorDrawables { 39 | useSupportLibrary true 40 | } 41 | 42 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 43 | // The following argument makes the Android Test Orchestrator run its 44 | // "pm clear" command after each test invocation. This command ensures 45 | // that the app's state is completely cleared between tests. 46 | testInstrumentationRunnerArguments clearPackageData: 'true' 47 | } 48 | 49 | buildTypes { 50 | release { 51 | minifyEnabled false 52 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 53 | } 54 | } 55 | compileOptions { 56 | sourceCompatibility JavaVersion.VERSION_17 57 | targetCompatibility JavaVersion.VERSION_17 58 | } 59 | kotlinOptions { 60 | jvmTarget = '17' 61 | } 62 | buildFeatures { 63 | buildConfig false 64 | compose true 65 | } 66 | 67 | testOptions { 68 | unitTests { 69 | includeAndroidResources = true 70 | } 71 | animationsDisabled true 72 | execution 'ANDROIDX_TEST_ORCHESTRATOR' 73 | } 74 | packagingOptions { 75 | resources { 76 | excludes += [ 77 | '/META-INF/AL2.0', 78 | '/META-INF/LGPL2.1' 79 | ] 80 | } 81 | } 82 | 83 | publishing { 84 | multipleVariants { 85 | allVariants() 86 | } 87 | } 88 | lint { 89 | checkReleaseBuilds false 90 | textOutput file('stdout') 91 | textReport true 92 | } 93 | } 94 | 95 | dependencies { 96 | implementation platform(libs.androidx.compose.bom) 97 | implementation libs.compose.ui.ui 98 | 99 | implementation libs.glance.appwidget 100 | } 101 | 102 | apply plugin: "com.vanniktech.maven.publish" -------------------------------------------------------------------------------- /appwidget-testing/gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | POM_ARTIFACT_ID=appwidget-testing 18 | POM_NAME=Glance Experimental Tools - AppWidget Testing 19 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /appwidget-testing/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /appwidget-testing/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 23 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /appwidget-testing/src/main/java/com/google/android/glance/appwidget/testing/GlanceScreenshotTestActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.appwidget.testing 18 | 19 | import android.app.Activity 20 | import android.appwidget.AppWidgetHostView 21 | import android.content.Context 22 | import android.graphics.Color 23 | import android.graphics.Rect 24 | import android.os.Build 25 | import android.os.Bundle 26 | import android.util.DisplayMetrics 27 | import android.util.TypedValue 28 | import android.view.Gravity 29 | import android.view.View 30 | import android.widget.FrameLayout 31 | import androidx.annotation.RequiresApi 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.ui.unit.Dp 34 | import androidx.compose.ui.unit.DpSize 35 | import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi 36 | import androidx.glance.appwidget.GlanceRemoteViews 37 | import kotlinx.coroutines.runBlocking 38 | 39 | /** 40 | * An activity that acts as a host for independently rendering glance composable content in 41 | * screenshot tests. 42 | * 43 | * See README.md for usage. 44 | * 45 | * NOTE: The device and screenshot framework you use should support hardware acceleration and 46 | * `clipToOutline` to see rounded corners. For robolectric, see 47 | * https://github.com/robolectric/robolectric/issues/8081#issuecomment-1478137890. 48 | * When using an emulator, you may use Espresso's `captureToBitmap` to ensure that the corner radius 49 | * is captured. 50 | */ 51 | @RequiresApi(Build.VERSION_CODES.O) 52 | public class GlanceScreenshotTestActivity : Activity() { 53 | private var state: Any? = null 54 | private var size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified) 55 | private var wrapContentSize: Boolean = false 56 | private lateinit var hostView: AppWidgetHostView 57 | 58 | override fun onCreate(savedInstanceState: Bundle?) { 59 | super.onCreate(savedInstanceState) 60 | setContentView(R.layout.test_activity_layout) 61 | } 62 | 63 | /** 64 | * Sets the appwidget state that can be accessed via LocalState composition local. 65 | */ 66 | public fun setState(state: T) { 67 | this.state = state 68 | } 69 | 70 | /** 71 | * Sets the size of appwidget to be assumed for the test. This corresponds to the "LocalSize" 72 | * composition local. 73 | * 74 | * Content will be rendered in this size, unless wrapContentSize was set. 75 | */ 76 | public fun setAppWidgetSize(size: DpSize) { 77 | this.size = size 78 | } 79 | 80 | /** 81 | * Sets the size of rendering area to wrap size of the composable under test instead of using 82 | * the same size as one provided in [setAppWidgetSize]. This is useful when you are testing a 83 | * small part of the appwidget independently. 84 | * 85 | * Note: Calling [wrapContentSize] doesn't impact "LocalSize" compositionLocal. Use 86 | * [setAppWidgetSize] to set the value that should be used for the compositionLocal. 87 | */ 88 | public fun wrapContentSize() { 89 | this.wrapContentSize = true 90 | } 91 | 92 | /** 93 | * Renders the given glance composable in the activity. 94 | * 95 | * Provide appwidget size before calling this. 96 | */ 97 | @OptIn(ExperimentalGlanceRemoteViewsApi::class) 98 | public fun renderComposable(composable: @Composable () -> Unit) { 99 | runBlocking { 100 | val remoteViews = GlanceRemoteViews().compose( 101 | context = applicationContext, 102 | size = size, 103 | state = state, 104 | content = composable 105 | ).remoteViews 106 | 107 | val activityFrame = findViewById(R.id.content) 108 | hostView = TestHostView(applicationContext) 109 | hostView.setBackgroundColor(Color.WHITE) 110 | activityFrame.addView(hostView) 111 | 112 | val view = remoteViews.apply(applicationContext, hostView) 113 | hostView.addView(view) 114 | 115 | adjustHostViewSize() 116 | } 117 | } 118 | 119 | private fun adjustHostViewSize() { 120 | val displayMetrics = resources.displayMetrics 121 | 122 | if (wrapContentSize) { 123 | hostView.layoutParams = FrameLayout.LayoutParams( 124 | FrameLayout.LayoutParams.WRAP_CONTENT, 125 | FrameLayout.LayoutParams.WRAP_CONTENT, 126 | Gravity.CENTER 127 | ) 128 | } else { 129 | val hostViewPadding = Rect() 130 | val width = 131 | size.width.toPixels(displayMetrics) + hostViewPadding.left + hostViewPadding.right 132 | val height = 133 | size.height.toPixels(displayMetrics) + hostViewPadding.top + hostViewPadding.bottom 134 | 135 | hostView.layoutParams = FrameLayout.LayoutParams(width, height, Gravity.CENTER) 136 | } 137 | 138 | hostView.requestLayout() 139 | } 140 | 141 | private fun Dp.toPixels(displayMetrics: DisplayMetrics) = 142 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics).toInt() 143 | 144 | @RequiresApi(Build.VERSION_CODES.O) 145 | private class TestHostView(context: Context) : AppWidgetHostView(context) { 146 | init { 147 | // Prevent asynchronous inflation of the App Widget 148 | setExecutor(null) 149 | layoutDirection = View.LAYOUT_DIRECTION_LOCALE 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /appwidget-testing/src/main/res/layout/test_activity_layout.xml: -------------------------------------------------------------------------------- 1 | 16 | 21 | -------------------------------------------------------------------------------- /appwidget-viewer/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /appwidget-viewer/README.md: -------------------------------------------------------------------------------- 1 | # AppWidget Viewer 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.google.android.glance.tools/appwidget-viewer)](https://search.maven.org/search?q=g:com.google.android.glance.tools) 4 | 5 | This module allows developers to speed up UI iterations and UI testing by 6 | providing 7 | an embedded activity that displays apps widgets, embedded inside the app (instead of the 8 | launcher), 9 | offering faster previews and benefiting from the “Apply Changes” and “Live Edits” from Android 10 | Studio, among other features. 11 | 12 | ## Details 13 | 14 | The viewer does not rely on 15 | the [AppWidgetManager](https://developer.android.com/reference/android/appwidget/AppWidgetManager) 16 | and skips the BroadcastReceiver mechanism to directly render the 17 | [RemoteViews](https://developer.android.com/reference/android/widget/RemoteViews) 18 | inside the app’s activity by using the AppWidgetHostView ([limitations](#Limitations)). 19 | 20 | This together with Compose 21 | and [Live Edits](https://developer.android.com/jetpack/compose/tooling#live-edit) 22 | we can achieve ([in most situations](https://developer.android.com/studio/run#limitations)) a 23 | real-time update mechanism, allowing developers to see the changes reflected nearly instantaneously. 24 | 25 | 26 | 27 | By embedding the app widget inside the app, we automatically enable available developer tools like 28 | [Layout Inspector](https://developer.android.com/jetpack/compose/tooling#layout-inspector). 29 | 30 | 31 | 32 | ### Setup 33 | 34 | The library is integrated in 3 simple steps: 35 | 36 | *First*, add it as debug dependency: 37 | 38 | ```groovy 39 | repositories { 40 | mavenCentral() 41 | } 42 | 43 | dependencies { 44 | debugImplementation "com.google.android.glance.tools:appwidget-viewer:" 45 | } 46 | ``` 47 | 48 | *Second*, create a debug activity inside the debug folder and register it in 49 | the `AndroidManifest.xml`: 50 | 51 | ```xml 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | > Note: setting the LAUNCHER intent-filter is optional but it will add a direct access in the device 62 | 63 | *Third*, provide the viewer information: 64 | 65 | ```kotlin 66 | class MyWidgetViewerActivity : GlanceViewerActivity() { 67 | 68 | override suspend fun getGlanceSnapshot( 69 | receiver: Class 70 | ): GlanceSnapshot { 71 | return when (receiver) { 72 | MyGlanceWidgetReceiver::class.java -> GlanceSnapshot( 73 | instance = MyGlanceWidget(), 74 | state = mutablePreferencesOf(intPreferencesKey("state") to value) 75 | ) 76 | else -> throw IllegalArgumentException() 77 | } 78 | } 79 | 80 | override fun getProviders() = listOf(MyGlanceWidgetReceiver::class.java) 81 | } 82 | ``` 83 | 84 | Now, you can launch the activity from Android Studio (change the run configuration) and view the 85 | changes. 86 | 87 | ### Additional features. 88 | 89 | #### Displays a list of provided apps widgets 90 | 91 | `GlanceViewerActivity` offers an API to pass a `GlanceAppWidget` instance (or RemoteView directly) 92 | enabling customization of the widget and allowing initialization of data/stuff before displaying 93 | widget (e.g setting a fake GlanceState) 94 | 95 | 96 | 97 | #### Resizing the widget and respecting SizeMode. 98 | 99 | Use the resize panel to adjust sizing attributes and ensure the UI fits in all modes/sizes. 100 | The example below shows a widget that displays the size value for each of the Glance SizeModes 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
SingleExactResponsive
123
114 | 115 | #### Highlight missing meta-data 116 | 117 | Use the info panel to display the AppWidgetProviderInfo metadata and highlight missing information 118 | 119 | 120 | 121 | #### Extract Viewer and share: 122 | 123 | Use the share button to export the current snapshot into a PNG image. This image will be stored 124 | inside the device gallery under "appwidget-viewers". These snapshot images can be used 125 | as `android:previewImage` metadata for the appwidget. 126 | 127 | To retrieve the files: 128 | 129 | 1. `adb pull sdcard/Pictures/appwidget-viewers .` 130 | 2. Use the [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer) 131 | in Android Studio 132 | 133 | 134 | 135 | ### Limitations 136 | 137 | The design works with the following limitations: 138 | 139 | * Actions callback, updates or updating the state do not work since the widget is not “hosted”. 140 | * The exact representation in the launcher might differ depending on the launcher implementation. 141 | * All “Live Edits” and “Apply Changes” limitations apply 142 | * e.g Android Studio electric Eel + Android 10+ is needed. 143 | * Some projects might not work with Apply changes or Live Edits. 144 | 145 | # Snapshots 146 | 147 | Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. 148 | These are updated on every commit. 149 | 150 | [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/android/glance/tools/appwidget-viewer/ -------------------------------------------------------------------------------- /appwidget-viewer/api/current.api: -------------------------------------------------------------------------------- 1 | // Signature format: 4.0 2 | package com.google.android.glance.tools.viewer { 3 | 4 | public abstract class AppWidgetViewerActivity extends androidx.activity.ComponentActivity { 5 | ctor public AppWidgetViewerActivity(); 6 | method public abstract suspend Object? getAppWidgetSnapshot(android.appwidget.AppWidgetProviderInfo info, long size, kotlin.coroutines.Continuation); 7 | method public abstract java.util.List> getProviders(); 8 | } 9 | 10 | public final class GlanceSnapshot { 11 | ctor public GlanceSnapshot(androidx.glance.appwidget.GlanceAppWidget instance, optional Object? state); 12 | method public androidx.glance.appwidget.GlanceAppWidget component1(); 13 | method public Object? component2(); 14 | method public com.google.android.glance.tools.viewer.GlanceSnapshot copy(androidx.glance.appwidget.GlanceAppWidget instance, Object? state); 15 | method public androidx.glance.appwidget.GlanceAppWidget getInstance(); 16 | method public Object? getState(); 17 | property public final androidx.glance.appwidget.GlanceAppWidget instance; 18 | property public final Object? state; 19 | } 20 | 21 | @androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi public abstract class GlanceViewerActivity extends com.google.android.glance.tools.viewer.AppWidgetViewerActivity { 22 | ctor public GlanceViewerActivity(); 23 | method public suspend Object? getAppWidgetSnapshot(android.appwidget.AppWidgetProviderInfo info, long size, kotlin.coroutines.Continuation); 24 | method public abstract suspend Object? getGlanceSnapshot(Class receiver, kotlin.coroutines.Continuation); 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /appwidget-viewer/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | id 'org.jetbrains.dokka' 21 | alias(libs.plugins.compose.compiler) 22 | } 23 | 24 | kotlin { 25 | } 26 | 27 | android { 28 | namespace 'com.google.android.glance.tools.viewer' 29 | compileSdk 35 30 | 31 | defaultConfig { 32 | minSdk 21 33 | targetSdk 35 34 | 35 | consumerProguardFiles "consumer-rules.pro" 36 | 37 | vectorDrawables { 38 | useSupportLibrary true 39 | } 40 | 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | // The following argument makes the Android Test Orchestrator run its 43 | // "pm clear" command after each test invocation. This command ensures 44 | // that the app's state is completely cleared between tests. 45 | testInstrumentationRunnerArguments clearPackageData: 'true' 46 | } 47 | 48 | buildTypes { 49 | release { 50 | minifyEnabled false 51 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 52 | } 53 | } 54 | compileOptions { 55 | sourceCompatibility JavaVersion.VERSION_17 56 | targetCompatibility JavaVersion.VERSION_17 57 | } 58 | kotlinOptions { 59 | jvmTarget = '17' 60 | } 61 | buildFeatures { 62 | buildConfig false 63 | compose true 64 | } 65 | 66 | testOptions { 67 | unitTests { 68 | includeAndroidResources = true 69 | } 70 | animationsDisabled true 71 | execution 'ANDROIDX_TEST_ORCHESTRATOR' 72 | } 73 | packagingOptions { 74 | resources { 75 | excludes += [ 76 | '/META-INF/AL2.0', 77 | '/META-INF/LGPL2.1' 78 | ] 79 | } 80 | } 81 | 82 | 83 | publishing { 84 | multipleVariants { 85 | allVariants() 86 | } 87 | } 88 | lint { 89 | checkReleaseBuilds false 90 | textOutput file('stdout') 91 | textReport true 92 | } 93 | } 94 | 95 | dependencies { 96 | implementation platform(libs.androidx.compose.bom) 97 | 98 | api project(":appwidget-host") 99 | 100 | compileOnly libs.glance.appwidget 101 | 102 | implementation libs.androidx.core 103 | implementation libs.androidx.activity.compose 104 | implementation libs.compose.ui.ui 105 | implementation libs.compose.material.material3 106 | implementation libs.compose.material.material 107 | implementation libs.compose.material.iconsext 108 | 109 | // ====================== 110 | // Test dependencies 111 | // ====================== 112 | 113 | androidTestUtil libs.androidx.test.orchestrator 114 | 115 | androidTestImplementation libs.androidx.activity.compose 116 | androidTestImplementation libs.compose.material.material 117 | 118 | androidTestImplementation libs.junit 119 | androidTestImplementation libs.truth 120 | 121 | androidTestImplementation libs.compose.ui.test.junit4 122 | androidTestImplementation libs.compose.ui.test.manifest 123 | androidTestImplementation libs.androidx.test.core 124 | androidTestImplementation libs.androidx.test.runner 125 | androidTestImplementation libs.androidx.test.rules 126 | androidTestImplementation libs.androidx.test.uiAutomator 127 | } 128 | 129 | apply plugin: "com.vanniktech.maven.publish" 130 | apply plugin: "me.tylerbwong.gradle.metalava" 131 | 132 | metalava { 133 | filename = "api/current.api" 134 | reportLintsAsErrors = true 135 | } 136 | -------------------------------------------------------------------------------- /appwidget-viewer/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/consumer-rules.pro -------------------------------------------------------------------------------- /appwidget-viewer/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=appwidget-viewer 2 | POM_NAME=Glance Experimental Tools - Viewer 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /appwidget-viewer/images/live-edit-showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/live-edit-showcase.gif -------------------------------------------------------------------------------- /appwidget-viewer/images/preview-device-file-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/preview-device-file-explorer.png -------------------------------------------------------------------------------- /appwidget-viewer/images/preview-info-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/preview-info-panel.png -------------------------------------------------------------------------------- /appwidget-viewer/images/preview-layout-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/preview-layout-inspector.png -------------------------------------------------------------------------------- /appwidget-viewer/images/preview-resize-exact.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/preview-resize-exact.gif -------------------------------------------------------------------------------- /appwidget-viewer/images/preview-resize-responsive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/preview-resize-responsive.gif -------------------------------------------------------------------------------- /appwidget-viewer/images/preview-resize-single.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/preview-resize-single.gif -------------------------------------------------------------------------------- /appwidget-viewer/images/preview-widget-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/appwidget-viewer/images/preview-widget-selector.png -------------------------------------------------------------------------------- /appwidget-viewer/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /appwidget-viewer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | -------------------------------------------------------------------------------- /appwidget-viewer/src/main/java/com/google/android/glance/tools/viewer/GlanceSnapshot.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.viewer 18 | 19 | import androidx.glance.appwidget.GlanceAppWidget 20 | 21 | /** 22 | * Data class containing a snapshot of the [GlanceAppWidget] and the associated state as defined 23 | * by the [GlanceAppWidget.stateDefinition] of the provided instance. 24 | */ 25 | data class GlanceSnapshot(val instance: GlanceAppWidget, val state: Any? = null) 26 | -------------------------------------------------------------------------------- /appwidget-viewer/src/main/java/com/google/android/glance/tools/viewer/GlanceViewerActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.viewer 18 | 19 | import android.appwidget.AppWidgetManager 20 | import android.appwidget.AppWidgetProvider 21 | import android.appwidget.AppWidgetProviderInfo 22 | import android.os.Build 23 | import android.os.Bundle 24 | import android.widget.RemoteViews 25 | import androidx.activity.ComponentActivity 26 | import androidx.activity.compose.setContent 27 | import androidx.annotation.CallSuper 28 | import androidx.compose.foundation.layout.fillMaxSize 29 | import androidx.compose.material3.MaterialTheme 30 | import androidx.compose.material3.Surface 31 | import androidx.compose.runtime.MutableState 32 | import androidx.compose.runtime.mutableStateOf 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.unit.DpSize 35 | import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi 36 | import androidx.glance.appwidget.GlanceAppWidget 37 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 38 | import androidx.glance.appwidget.compose 39 | import com.google.android.glance.appwidget.host.getTargetSize 40 | import com.google.android.glance.tools.viewer.ui.ViewerScreen 41 | import com.google.android.glance.tools.viewer.ui.theme.ViewerTheme 42 | import kotlinx.coroutines.Dispatchers 43 | import kotlinx.coroutines.withContext 44 | 45 | /** 46 | * Base class to display AppWidgets. 47 | */ 48 | abstract class AppWidgetViewerActivity : ComponentActivity() { 49 | 50 | /** 51 | * The list of [AppWidgetProvider] to display in the viewer 52 | */ 53 | abstract fun getProviders(): List> 54 | 55 | /** 56 | * Provides the [RemoteViews] snapshot of the given [AppWidgetProviderInfo] for the given size 57 | * 58 | * @param - The [AppWidgetProviderInfo] containing the metadata of the appwidget 59 | * @param - The available size to display the appwidget 60 | * 61 | * @return the [RemoteViews] instance to use for the viewer. 62 | */ 63 | abstract suspend fun getAppWidgetSnapshot( 64 | info: AppWidgetProviderInfo, 65 | size: DpSize 66 | ): RemoteViews 67 | 68 | // Moving states outside of composition to ensure they are kept when Live Edits happen. 69 | private lateinit var selectedProvider: MutableState 70 | private lateinit var currentSize: MutableState 71 | 72 | @CallSuper 73 | override fun onCreate(savedInstanceState: Bundle?) { 74 | super.onCreate(savedInstanceState) 75 | val widgetManager = AppWidgetManager.getInstance(this) 76 | val providers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 77 | widgetManager.getInstalledProvidersForPackage(packageName, null) 78 | } else { 79 | widgetManager.installedProviders.filter { it.provider.packageName == packageName } 80 | }.filter { info -> 81 | getProviders().any { selectedProvider -> 82 | selectedProvider.name == info.provider.className 83 | } 84 | } 85 | 86 | selectedProvider = mutableStateOf(providers.first()) 87 | currentSize = mutableStateOf(selectedProvider.value.getTargetSize(this)) 88 | 89 | setContent { 90 | ViewerTheme { 91 | Surface( 92 | modifier = Modifier.fillMaxSize(), 93 | color = MaterialTheme.colorScheme.background 94 | ) { 95 | ViewerScreen( 96 | providers = providers, 97 | selectedProvider = selectedProvider.value, 98 | currentSize = currentSize.value, 99 | snapshot = ::getAppWidgetSnapshot, 100 | onResize = { currentSize.value = it }, 101 | onSelected = { selectedProvider.value = it } 102 | ) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Extend this activity to provide a set of GlanceAppWidget snapshots to display. 111 | */ 112 | @ExperimentalGlanceRemoteViewsApi 113 | abstract class GlanceViewerActivity : AppWidgetViewerActivity() { 114 | 115 | /** 116 | * Provides an instance of [GlanceAppWidget] to display inside the viewer. 117 | * 118 | * @param receiver - The selected [GlanceAppWidgetReceiver] to display 119 | */ 120 | abstract suspend fun getGlanceSnapshot(receiver: Class): GlanceSnapshot 121 | 122 | /** 123 | * Only override this method to directly provide [RemoteViews] instead of [GlanceAppWidget] 124 | * instances. 125 | * 126 | * @see AppWidgetViewerActivity.getAppWidgetSnapshot 127 | */ 128 | @Suppress("UNCHECKED_CAST") 129 | override suspend fun getAppWidgetSnapshot( 130 | info: AppWidgetProviderInfo, 131 | size: DpSize 132 | ): RemoteViews = withContext(Dispatchers.IO) { 133 | val receiver = Class.forName(info.provider.className) 134 | require(GlanceAppWidgetReceiver::class.java.isAssignableFrom(receiver)) { 135 | "AppWidget is not a GlanceAppWidgetReceiver. Override this method to provide other implementations" 136 | } 137 | 138 | val receiverClass = receiver as Class 139 | val snapshot = getGlanceSnapshot(receiverClass) 140 | snapshot.instance.compose(applicationContext, size = size, state = snapshot.state) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /appwidget-viewer/src/main/java/com/google/android/glance/tools/viewer/ui/ViewerPanel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.viewer.ui 18 | 19 | internal enum class ViewerPanel { 20 | Resize, Info 21 | } 22 | -------------------------------------------------------------------------------- /appwidget-viewer/src/main/java/com/google/android/glance/tools/viewer/ui/ViewerResizePanel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.viewer.ui 18 | 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.Spacer 21 | import androidx.compose.foundation.layout.fillMaxWidth 22 | import androidx.compose.foundation.layout.padding 23 | import androidx.compose.foundation.layout.size 24 | import androidx.compose.material.Text 25 | import androidx.compose.material3.MaterialTheme 26 | import androidx.compose.material3.Slider 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.platform.LocalConfiguration 30 | import androidx.compose.ui.unit.DpSize 31 | import androidx.compose.ui.unit.dp 32 | 33 | @Composable 34 | internal fun ViewerResizePanel( 35 | currentSize: DpSize, 36 | onSizeChange: (DpSize) -> Unit 37 | ) { 38 | // TODO probably we should get the real max available size from the layout one measured 39 | val configuration = LocalConfiguration.current 40 | Column( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .padding(16.dp) 44 | ) { 45 | Text( 46 | text = "Resize widget:", 47 | style = MaterialTheme.typography.titleMedium, 48 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) 49 | ) 50 | 51 | val padding = Modifier.padding(start = 16.dp, end = 16.dp) 52 | Text( 53 | text = "Width: ${currentSize.width.value.toInt()}dp", 54 | style = MaterialTheme.typography.labelMedium, 55 | modifier = padding 56 | ) 57 | Slider( 58 | modifier = padding, 59 | value = currentSize.width.value, 60 | valueRange = 48f..configuration.screenWidthDp.toFloat(), 61 | onValueChange = { 62 | onSizeChange(currentSize.copy(width = it.dp)) 63 | } 64 | ) 65 | Spacer(modifier = Modifier.size(8.dp)) 66 | Text( 67 | text = "Height: ${currentSize.height.value.toInt()}dp", 68 | style = MaterialTheme.typography.labelMedium, 69 | modifier = padding 70 | ) 71 | Slider( 72 | modifier = padding, 73 | value = currentSize.height.value, 74 | valueRange = 48f..configuration.screenHeightDp.toFloat(), 75 | onValueChange = { onSizeChange(currentSize.copy(height = it.dp)) } 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /appwidget-viewer/src/main/java/com/google/android/glance/tools/viewer/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.viewer.ui.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | internal val Purple80 = Color(0xFFD0BCFF) 22 | internal val PurpleGrey80 = Color(0xFFCCC2DC) 23 | internal val Pink80 = Color(0xFFEFB8C8) 24 | 25 | internal val Purple40 = Color(0xFF6650a4) 26 | internal val PurpleGrey40 = Color(0xFF625b71) 27 | internal val Pink40 = Color(0xFF7D5260) 28 | -------------------------------------------------------------------------------- /appwidget-viewer/src/main/java/com/google/android/glance/tools/viewer/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.viewer.ui.theme 18 | 19 | import android.app.Activity 20 | import android.os.Build 21 | import androidx.compose.foundation.isSystemInDarkTheme 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.darkColorScheme 24 | import androidx.compose.material3.dynamicDarkColorScheme 25 | import androidx.compose.material3.dynamicLightColorScheme 26 | import androidx.compose.material3.lightColorScheme 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.SideEffect 29 | import androidx.compose.ui.graphics.toArgb 30 | import androidx.compose.ui.platform.LocalContext 31 | import androidx.compose.ui.platform.LocalView 32 | import androidx.core.view.WindowCompat 33 | 34 | private val DarkColorScheme = darkColorScheme( 35 | primary = Purple80, 36 | secondary = PurpleGrey80, 37 | tertiary = Pink80 38 | ) 39 | 40 | private val LightColorScheme = lightColorScheme( 41 | primary = Purple40, 42 | secondary = PurpleGrey40, 43 | tertiary = Pink40 44 | ) 45 | 46 | @Composable 47 | internal fun ViewerTheme( 48 | darkTheme: Boolean = isSystemInDarkTheme(), 49 | // Dynamic color is available on Android 12+ 50 | dynamicColor: Boolean = true, 51 | content: @Composable () -> Unit 52 | ) { 53 | val colorScheme = when { 54 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 55 | val context = LocalContext.current 56 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 57 | } 58 | darkTheme -> DarkColorScheme 59 | else -> LightColorScheme 60 | } 61 | val view = LocalView.current 62 | if (!view.isInEditMode) { 63 | SideEffect { 64 | val window = (view.context as Activity).window 65 | window.statusBarColor = colorScheme.primary.toArgb() 66 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 67 | } 68 | } 69 | 70 | MaterialTheme( 71 | colorScheme = colorScheme, 72 | typography = Typography, 73 | content = content 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /appwidget-viewer/src/main/java/com/google/android/glance/tools/viewer/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.viewer.ui.theme 18 | 19 | import androidx.compose.material3.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.sp 24 | 25 | // Set of Material typography styles to start with 26 | internal val Typography = Typography( 27 | bodyLarge = TextStyle( 28 | fontFamily = FontFamily.Default, 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 16.sp, 31 | lineHeight = 24.sp, 32 | letterSpacing = 0.5.sp 33 | ) 34 | ) 35 | -------------------------------------------------------------------------------- /checksum.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2022 The Android Open Source Project 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 | RESULT_FILE=$1 20 | 21 | if [ -f $RESULT_FILE ]; then 22 | rm $RESULT_FILE 23 | fi 24 | touch $RESULT_FILE 25 | 26 | checksum_file() { 27 | echo $(openssl md5 $1 | awk '{print $2}') 28 | } 29 | 30 | FILES=() 31 | while read -r -d ''; do 32 | FILES+=("$REPLY") 33 | done < <(find . -type f \( -name "build.gradle*" -o -name "*.versions.toml" -o -name "gradle-wrapper.properties" \) -print0) 34 | 35 | # Loop through files and append MD5 to result file 36 | for FILE in ${FILES[@]}; do 37 | echo $(checksum_file $FILE) >> $RESULT_FILE 38 | done 39 | # Now sort the file so that it is idempotent 40 | sort $RESULT_FILE -o $RESULT_FILE 41 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Turn on parallel compilation, caching and on-demand configuration 18 | org.gradle.configureondemand=true 19 | org.gradle.caching=true 20 | org.gradle.parallel=true 21 | 22 | # Declare we support AndroidX 23 | android.useAndroidX=true 24 | 25 | # Increase memory 26 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError 27 | 28 | # Required to publish to Nexus (see https://github.com/gradle/gradle/issues/11308) 29 | systemProp.org.gradle.internal.publish.checksums.insecure=true 30 | 31 | # Increase timeout when pushing to Sonatype (otherwise we get timeouts) 32 | systemProp.org.gradle.internal.http.socketTimeout=120000 33 | 34 | GROUP=com.google.android.glance.tools 35 | VERSION_NAME=0.2.3-SNAPSHOT 36 | 37 | POM_DESCRIPTION=Experimental tools for Jetpack Glance 38 | 39 | POM_URL=https://github.com/google/glance-experimental-tools/ 40 | POM_SCM_URL=https://github.com/google/glance-experimental-tools/ 41 | POM_SCM_CONNECTION=scm:git:git://github.com/google/glance-experimental-tools.git 42 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/google/glance-experimental-tools.git 43 | 44 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 45 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 46 | POM_LICENCE_DIST=repo 47 | 48 | POM_DEVELOPER_ID=google 49 | POM_DEVELOPER_NAME=Google 50 | android.defaults.buildfeatures.buildconfig=true 51 | android.nonTransitiveRClass=false 52 | android.nonFinalResIds=false 53 | 54 | # Enable Roborazzi screenshot tests 55 | roborazzi.test.record=true 56 | roborazzi.test.compare=true 57 | roborazzi.test.verify=true 58 | 59 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | composeCompiler = "1.5.3" 3 | composesnapshot = "-" # a single character = no snapshot 4 | androidx-compose-bom = "2024.12.01" 5 | 6 | glance = "1.1.1" 7 | glancesnapshot = "12873994" # a single character = no snapshot 8 | 9 | # gradlePlugin and lint need to be updated together 10 | gradlePlugin = "8.7.3" 11 | lintMinCompose = "31.7.3" 12 | 13 | ktlint = "0.42.1" 14 | kotlin = "2.1.0" 15 | coroutines = "1.9.0" 16 | 17 | androidxtest-core = "1.6.1" 18 | androidxtest-rules = "1.6.1" 19 | androidxtest-orchestrator = "1.5.1" 20 | androidxtest-runner = "1.6.2" 21 | 22 | androidx-test-ext-junit = "1.2.1" 23 | 24 | robolectric = "4.14.1" 25 | roborazzi = "1.26.0" 26 | 27 | [libraries] 28 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } 29 | compose-ui-ui = { module = "androidx.compose.ui:ui" } 30 | compose-ui-util = { module = "androidx.compose.ui:ui-util" } 31 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 32 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } 33 | compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } 34 | compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } 35 | compose-foundation-foundation = { module = "androidx.compose.foundation:foundation" } 36 | compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } 37 | compose-material-material = { module = "androidx.compose.material:material" } 38 | compose-material-iconsext = { module = "androidx.compose.material:material-icons-extended" } 39 | compose-animation-animation = { module = "androidx.compose.animation:animation" } 40 | compose-material-material3 = { module = "androidx.compose.material3:material3" } 41 | 42 | glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" } 43 | 44 | google-material = "com.google.android.material:material:1.12.0" 45 | 46 | android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "gradlePlugin" } 47 | gradleMavenPublishPlugin = "com.vanniktech:gradle-maven-publish-plugin:0.25.3" 48 | metalavaGradle = "me.tylerbwong.gradle.metalava:plugin:0.3.5" 49 | 50 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 51 | kotlin-stdlibJdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } 52 | kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 53 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 54 | 55 | kotlin-metadataJvm = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.3.0" 56 | 57 | kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } 58 | kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 59 | 60 | dokka = "org.jetbrains.dokka:dokka-gradle-plugin:1.9.10" 61 | 62 | androidx-core = "androidx.core:core-ktx:1.15.0" 63 | androidx-core-remoteviews = "androidx.core:core-remoteviews:1.1.0" 64 | androidx-activity-compose = "androidx.activity:activity-compose:1.9.3" 65 | 66 | androidx-test-core = { module = "androidx.test:core-ktx", version.ref = "androidxtest-core" } 67 | androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxtest-runner" } 68 | androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxtest-rules" } 69 | androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidxtest-orchestrator" } 70 | androidx-test-uiAutomator = "androidx.test.uiautomator:uiautomator:2.3.0" 71 | 72 | # alpha for robolectric x compose fix 73 | androidx-test-espressoCore = "androidx.test.espresso:espresso-core:3.6.1" 74 | androidx-test-espressoWeb = "androidx.test.espresso:espresso-web:3.6.1" 75 | androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } 76 | 77 | junit = "junit:junit:4.13.2" 78 | truth = "com.google.truth:truth:1.1.2" 79 | 80 | robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } 81 | roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi"} 82 | 83 | affectedmoduledetector = "com.dropbox.affectedmoduledetector:affectedmoduledetector:0.1.2" 84 | 85 | android-tools-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradlePlugin" } 86 | android-tools-lint-lint = { module = "com.android.tools.lint:lint", version.ref = "lintMinCompose" } 87 | android-tools-lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lintMinCompose" } 88 | android-tools-lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "lintMinCompose" } 89 | [plugins] 90 | roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi"} 91 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 92 | org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 93 | 94 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 25 11:00:02 CEST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /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 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /release/secring.gpg.aes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/release/secring.gpg.aes -------------------------------------------------------------------------------- /release/signing-cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2021 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | rm -f release/*.gpg 18 | rm -f release/*.properties 19 | -------------------------------------------------------------------------------- /release/signing-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2021 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | ENCRYPT_KEY=$1 18 | 19 | if [[ ! -z "$ENCRYPT_KEY" ]]; then 20 | openssl aes-256-cbc -md sha256 -d -in release/secring.gpg.aes -out release/secring.gpg -k ${ENCRYPT_KEY} 21 | openssl aes-256-cbc -md sha256 -d -in release/signing.properties.aes -out release/signing.properties -k ${ENCRYPT_KEY} 22 | else 23 | echo "ENCRYPT_KEY is empty" 24 | fi 25 | -------------------------------------------------------------------------------- /release/signing.properties.aes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/release/signing.properties.aes -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.application' 19 | id 'org.jetbrains.kotlin.android' 20 | alias(libs.plugins.roborazzi) 21 | alias(libs.plugins.compose.compiler) 22 | } 23 | 24 | android { 25 | namespace 'com.google.android.glance.tools.sample' 26 | compileSdk 35 27 | 28 | defaultConfig { 29 | applicationId "com.google.android.glance.tools.sample" 30 | minSdk 26 31 | targetSdk 35 32 | versionCode 1 33 | versionName "1.0" 34 | 35 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 36 | vectorDrawables { 37 | useSupportLibrary true 38 | } 39 | } 40 | 41 | buildTypes { 42 | release { 43 | minifyEnabled false 44 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 45 | } 46 | } 47 | compileOptions { 48 | sourceCompatibility JavaVersion.VERSION_17 49 | targetCompatibility JavaVersion.VERSION_17 50 | } 51 | kotlinOptions { 52 | jvmTarget = '17' 53 | } 54 | buildFeatures { 55 | compose true 56 | } 57 | 58 | packagingOptions { 59 | resources { 60 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 61 | } 62 | } 63 | testOptions { 64 | unitTests { 65 | includeAndroidResources = true 66 | } 67 | } 68 | } 69 | 70 | dependencies { 71 | implementation project(':appwidget-configuration') 72 | debugImplementation project(':appwidget-viewer') 73 | debugImplementation project(':appwidget-testing') 74 | 75 | implementation platform(libs.androidx.compose.bom) 76 | implementation libs.androidx.core 77 | implementation libs.androidx.core.remoteviews 78 | implementation libs.glance.appwidget 79 | implementation libs.google.material 80 | 81 | implementation libs.androidx.activity.compose 82 | implementation libs.compose.ui.ui 83 | implementation libs.compose.ui.tooling.preview 84 | implementation libs.compose.material.material3 85 | implementation libs.compose.material.iconsext 86 | 87 | debugImplementation libs.compose.ui.tooling 88 | implementation libs.compose.ui.tooling.preview 89 | 90 | testImplementation libs.junit 91 | testImplementation libs.androidx.test.core 92 | testImplementation libs.androidx.test.runner 93 | testImplementation libs.androidx.test.espressoCore 94 | testImplementation libs.androidx.test.rules 95 | testImplementation libs.androidx.test.ext.junit 96 | testImplementation libs.robolectric 97 | testImplementation libs.roborazzi 98 | } 99 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/debug/java/com/google/android/glance/tools/sample/SampleViewerActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.sample 18 | 19 | import android.appwidget.AppWidgetProviderInfo 20 | import android.widget.RemoteViews 21 | import androidx.compose.ui.unit.DpSize 22 | import androidx.datastore.preferences.core.mutablePreferencesOf 23 | import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi 24 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 25 | import com.google.android.glance.tools.viewer.GlanceSnapshot 26 | import com.google.android.glance.tools.viewer.GlanceViewerActivity 27 | 28 | @OptIn(ExperimentalGlanceRemoteViewsApi::class) 29 | class SampleViewerActivity : GlanceViewerActivity() { 30 | 31 | private var counter = 0 32 | 33 | override fun getProviders() = listOf( 34 | SampleGlanceWidgetReceiver::class.java, 35 | SampleAppWidgetReceiver::class.java 36 | ) 37 | 38 | override suspend fun getGlanceSnapshot( 39 | receiver: Class 40 | ): GlanceSnapshot { 41 | return when (receiver) { 42 | SampleGlanceWidgetReceiver::class.java -> GlanceSnapshot( 43 | instance = SampleGlanceWidget, 44 | state = mutablePreferencesOf( 45 | SampleGlanceWidget.countKey to counter++ 46 | ) 47 | ) 48 | else -> throw IllegalArgumentException() 49 | } 50 | } 51 | 52 | /** 53 | * To support non-glance widgets we can override this method and provide the RemoteViews directly 54 | */ 55 | override suspend fun getAppWidgetSnapshot( 56 | info: AppWidgetProviderInfo, 57 | size: DpSize 58 | ): RemoteViews { 59 | return when (info.provider.className) { 60 | SampleAppWidgetReceiver::class.java.name -> SampleAppWidget.createWidget(this) 61 | else -> super.getAppWidgetSnapshot(info, size) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /sample/src/main/java/com/google/android/glance/tools/sample/AppWidgetConfigurationActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.sample 18 | 19 | import android.app.Activity 20 | import android.os.Build 21 | import android.os.Bundle 22 | import androidx.activity.ComponentActivity 23 | import androidx.activity.compose.setContent 24 | import androidx.compose.foundation.isSystemInDarkTheme 25 | import androidx.compose.foundation.layout.Arrangement 26 | import androidx.compose.foundation.layout.Row 27 | import androidx.compose.foundation.layout.fillMaxWidth 28 | import androidx.compose.foundation.layout.padding 29 | import androidx.compose.material.icons.Icons 30 | import androidx.compose.material.icons.rounded.Add 31 | import androidx.compose.material.icons.rounded.Done 32 | import androidx.compose.material.icons.rounded.Remove 33 | import androidx.compose.material3.ExperimentalMaterial3Api 34 | import androidx.compose.material3.FloatingActionButton 35 | import androidx.compose.material3.Icon 36 | import androidx.compose.material3.IconButton 37 | import androidx.compose.material3.MaterialTheme 38 | import androidx.compose.material3.Text 39 | import androidx.compose.material3.darkColorScheme 40 | import androidx.compose.material3.dynamicDarkColorScheme 41 | import androidx.compose.material3.dynamicLightColorScheme 42 | import androidx.compose.material3.lightColorScheme 43 | import androidx.compose.runtime.Composable 44 | import androidx.compose.runtime.SideEffect 45 | import androidx.compose.runtime.rememberCoroutineScope 46 | import androidx.compose.ui.Alignment 47 | import androidx.compose.ui.Modifier 48 | import androidx.compose.ui.graphics.toArgb 49 | import androidx.compose.ui.platform.LocalContext 50 | import androidx.compose.ui.platform.LocalView 51 | import androidx.compose.ui.unit.dp 52 | import androidx.core.view.WindowCompat 53 | import androidx.datastore.preferences.core.Preferences 54 | import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi 55 | import com.google.android.glance.appwidget.configuration.AppWidgetConfigurationScaffold 56 | import com.google.android.glance.appwidget.configuration.AppWidgetConfigurationState 57 | import com.google.android.glance.appwidget.configuration.rememberAppWidgetConfigurationState 58 | import kotlinx.coroutines.launch 59 | 60 | class AppWidgetConfigurationActivity : ComponentActivity() { 61 | 62 | override fun onCreate(savedInstanceState: Bundle?) { 63 | super.onCreate(savedInstanceState) 64 | setContent { 65 | SampleTheme { 66 | SampleConfigScreen() 67 | } 68 | } 69 | } 70 | 71 | @Composable 72 | private fun SampleTheme(content: @Composable () -> Unit) { 73 | val darkTheme = isSystemInDarkTheme() 74 | val colorScheme = when { 75 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 76 | val context = LocalContext.current 77 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 78 | } 79 | darkTheme -> darkColorScheme() 80 | else -> lightColorScheme() 81 | } 82 | val view = LocalView.current 83 | if (!view.isInEditMode) { 84 | SideEffect { 85 | val window = (view.context as Activity).window 86 | window.statusBarColor = colorScheme.primary.toArgb() 87 | WindowCompat.getInsetsController(window, view).apply { 88 | isAppearanceLightStatusBars = darkTheme 89 | } 90 | } 91 | } 92 | 93 | MaterialTheme(colorScheme = colorScheme, content = content) 94 | } 95 | } 96 | 97 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalGlanceRemoteViewsApi::class) 98 | @Composable 99 | private fun SampleConfigScreen() { 100 | val scope = rememberCoroutineScope() 101 | val configurationState = rememberAppWidgetConfigurationState(SampleGlanceWidget) 102 | 103 | // If we don't have a valid id, discard configuration and finish the activity. 104 | if (configurationState.glanceId == null) { 105 | configurationState.discardConfiguration() 106 | return 107 | } 108 | 109 | AppWidgetConfigurationScaffold( 110 | appWidgetConfigurationState = configurationState, 111 | floatingActionButton = { 112 | FloatingActionButton(onClick = { 113 | scope.launch { 114 | configurationState.applyConfiguration() 115 | } 116 | }) { 117 | Icon(imageVector = Icons.Rounded.Done, contentDescription = "Save changes") 118 | } 119 | } 120 | ) { 121 | ConfigurationList(Modifier.padding(it), configurationState) 122 | } 123 | } 124 | 125 | @Composable 126 | private fun ConfigurationList(modifier: Modifier, state: AppWidgetConfigurationState) { 127 | fun updatePreferences(key: Preferences.Key, value: T) { 128 | state.updateCurrentState { 129 | it.toMutablePreferences().apply { 130 | set(key, value) 131 | }.toPreferences() 132 | } 133 | } 134 | 135 | val counter = state.getCurrentState()?.get(SampleGlanceWidget.countKey) ?: 0 136 | 137 | Row( 138 | modifier = modifier 139 | .fillMaxWidth() 140 | .padding(16.dp), 141 | horizontalArrangement = Arrangement.Center, 142 | verticalAlignment = Alignment.CenterVertically 143 | ) { 144 | Text("Setup counter:") 145 | IconButton(onClick = { 146 | updatePreferences(SampleGlanceWidget.countKey, counter + 1) 147 | }) { 148 | Icon(imageVector = Icons.Rounded.Add, contentDescription = "Add") 149 | } 150 | Text("$counter") 151 | IconButton(onClick = { 152 | updatePreferences(SampleGlanceWidget.countKey, counter - 1) 153 | }) { 154 | Icon(imageVector = Icons.Rounded.Remove, contentDescription = "Subtract") 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /sample/src/main/java/com/google/android/glance/tools/sample/SampleAppWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.sample 18 | 19 | import android.appwidget.AppWidgetManager 20 | import android.appwidget.AppWidgetProvider 21 | import android.content.Context 22 | import android.widget.RemoteViews 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.tooling.preview.Preview 27 | import androidx.compose.ui.unit.DpSize 28 | import androidx.compose.ui.unit.dp 29 | import com.google.android.glance.appwidget.host.AppWidgetHostPreview 30 | 31 | class SampleAppWidgetReceiver : AppWidgetProvider() { 32 | 33 | override fun onUpdate( 34 | context: Context, 35 | appWidgetManager: AppWidgetManager, 36 | appWidgetIds: IntArray 37 | ) { 38 | super.onUpdate(context, appWidgetManager, appWidgetIds) 39 | appWidgetManager.updateAppWidget(appWidgetIds, SampleAppWidget.createWidget(context)) 40 | } 41 | } 42 | 43 | object SampleAppWidget { 44 | fun createWidget(context: Context): RemoteViews { 45 | return RemoteViews( 46 | context.packageName, 47 | R.layout.widget_sample 48 | ) 49 | } 50 | } 51 | 52 | @Preview 53 | @Composable 54 | fun SampleAppWidgetPreview() { 55 | AppWidgetHostPreview( 56 | modifier = Modifier.fillMaxSize(), 57 | displaySize = DpSize(200.dp, 200.dp) 58 | ) { context -> 59 | SampleAppWidget.createWidget(context) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sample/src/main/java/com/google/android/glance/tools/sample/SampleGlanceWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.sample 18 | 19 | import android.content.Context 20 | import androidx.compose.foundation.layout.fillMaxSize 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.DpSize 26 | import androidx.compose.ui.unit.dp 27 | import androidx.datastore.preferences.core.intPreferencesKey 28 | import androidx.datastore.preferences.core.preferencesOf 29 | import androidx.glance.GlanceId 30 | import androidx.glance.GlanceModifier 31 | import androidx.glance.Image 32 | import androidx.glance.ImageProvider 33 | import androidx.glance.LocalSize 34 | import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi 35 | import androidx.glance.appwidget.GlanceAppWidget 36 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 37 | import androidx.glance.appwidget.SizeMode 38 | import androidx.glance.appwidget.cornerRadius 39 | import androidx.glance.appwidget.provideContent 40 | import androidx.glance.background 41 | import androidx.glance.currentState 42 | import androidx.glance.layout.Alignment 43 | import androidx.glance.layout.Column 44 | import androidx.glance.layout.Row 45 | import androidx.glance.layout.Spacer 46 | import androidx.glance.layout.fillMaxSize 47 | import androidx.glance.layout.padding 48 | import androidx.glance.layout.width 49 | import androidx.glance.text.Text 50 | import androidx.glance.text.TextAlign 51 | import androidx.glance.text.TextDecoration 52 | import androidx.glance.text.TextStyle 53 | import com.google.android.glance.appwidget.host.glance.GlanceAppWidgetHostPreview 54 | 55 | class SampleGlanceWidgetReceiver : GlanceAppWidgetReceiver() { 56 | 57 | override val glanceAppWidget: GlanceAppWidget = SampleGlanceWidget 58 | } 59 | 60 | object SampleGlanceWidget : GlanceAppWidget() { 61 | 62 | val countKey = intPreferencesKey("count") 63 | 64 | override val sizeMode: SizeMode = SizeMode.Exact 65 | override suspend fun provideGlance(context: Context, id: GlanceId) { 66 | provideContent { SampleGlanceWidgetContent() } 67 | } 68 | } 69 | 70 | @Composable 71 | fun SampleGlanceWidgetContent() { 72 | Column( 73 | modifier = GlanceModifier 74 | .fillMaxSize() 75 | .background(Color.White) 76 | .cornerRadius(16.dp) 77 | .padding(8.dp), 78 | verticalAlignment = Alignment.CenterVertically, 79 | horizontalAlignment = Alignment.CenterHorizontally, 80 | ) { 81 | val count = currentState(SampleGlanceWidget.countKey) ?: 0 82 | val size = LocalSize.current 83 | 84 | CountRow(count) 85 | SizeText(size) 86 | } 87 | } 88 | 89 | @Composable 90 | fun CountRow(count: Int) { 91 | Row(verticalAlignment = Alignment.CenterVertically) { 92 | Image( 93 | provider = ImageProvider(R.drawable.ic_android), 94 | contentDescription = "android icon", 95 | ) 96 | Spacer(modifier = GlanceModifier.width(8.dp)) 97 | Text( 98 | text = "Count: $count", 99 | style = TextStyle( 100 | textAlign = TextAlign.Center, 101 | textDecoration = TextDecoration.Underline 102 | ) 103 | ) 104 | } 105 | } 106 | 107 | @Composable 108 | fun SizeText(size: DpSize) { 109 | Text( 110 | text = "${size.width.value.toInt()} - ${size.height.value.toInt()}", 111 | style = TextStyle(textAlign = TextAlign.Center) 112 | ) 113 | } 114 | 115 | @OptIn(ExperimentalGlanceRemoteViewsApi::class) 116 | @Preview 117 | @Composable 118 | fun SampleGlanceWidgetPreview() { 119 | // The size of the widget 120 | val displaySize = DpSize(200.dp, 200.dp) 121 | // Provide a state depending on the GlanceAppWidget state definition 122 | val state = preferencesOf(SampleGlanceWidget.countKey to 2) 123 | 124 | GlanceAppWidgetHostPreview( 125 | modifier = Modifier.fillMaxSize(), 126 | glanceAppWidget = SampleGlanceWidget, 127 | state = state, 128 | displaySize = displaySize, 129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 25 | 31 | 34 | 37 | 38 | 39 | 40 | 46 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/app_widget_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/drawable/app_widget_preview.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/glance_widget_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/drawable/glance_widget_preview.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_android.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 176 | 181 | 186 | 187 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/widget_loading.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/widget_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Glance Experimental Tools 19 | Count: 20 | -------------------------------------------------------------------------------- /sample/src/main/res/xml/app_widget_sample.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /sample/src/main/res/xml/glance_widget_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/test/java/com/google/android/glance/tools/testing/SampleGlanceScreenshotTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.android.glance.tools.testing 18 | 19 | import androidx.compose.ui.unit.DpSize 20 | import androidx.compose.ui.unit.dp 21 | import androidx.datastore.preferences.core.preferencesOf 22 | import androidx.test.espresso.Espresso.onView 23 | import androidx.test.espresso.matcher.ViewMatchers 24 | import androidx.test.ext.junit.rules.ActivityScenarioRule 25 | import androidx.test.ext.junit.runners.AndroidJUnit4 26 | import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers 27 | import com.github.takahirom.roborazzi.RoborazziOptions 28 | import com.github.takahirom.roborazzi.captureRoboImage 29 | import com.google.android.glance.appwidget.testing.GlanceScreenshotTestActivity 30 | import com.google.android.glance.tools.sample.SampleGlanceWidget 31 | import com.google.android.glance.tools.sample.SampleGlanceWidgetContent 32 | import org.junit.Rule 33 | import org.junit.Test 34 | import org.junit.runner.RunWith 35 | import org.robolectric.annotation.Config 36 | import org.robolectric.annotation.GraphicsMode 37 | 38 | @RunWith(AndroidJUnit4::class) 39 | @GraphicsMode(GraphicsMode.Mode.NATIVE) 40 | @Config(sdk = [35], qualifiers = RobolectricDeviceQualifiers.Pixel6) 41 | class SampleGlanceScreenshotTest { 42 | 43 | @get:Rule 44 | val activityScenarioRule = 45 | ActivityScenarioRule(GlanceScreenshotTestActivity::class.java) 46 | 47 | @Test 48 | fun sampleGlanceContent() { 49 | renderComposable() 50 | 51 | // NOTE: The rendering and screenshot framework you use should support hardware acceleration 52 | // and `clipToOutline` to see rounded corners. For robolectric, see this 53 | // [issue](https://github.com/robolectric/robolectric/issues/8081#issuecomment-1478137890). 54 | // When using an emulator, you may use Espresso's `captureToBitmap` to ensure that the 55 | // corner radius is captured. 56 | captureAndVerifyScreenshot("sample_content") 57 | } 58 | 59 | @Test 60 | @Config(qualifiers = "+ar-ldrtl") 61 | fun sampleGlanceContent_rtl() { 62 | renderComposable() 63 | 64 | captureAndVerifyScreenshot("sample_content_rtl") 65 | } 66 | 67 | private fun renderComposable(size: DpSize = TEST_SIZE) { 68 | activityScenarioRule.scenario.onActivity { 69 | it.setAppWidgetSize(size) 70 | it.setState(preferencesOf(SampleGlanceWidget.countKey to 2)) 71 | 72 | it.renderComposable { 73 | SampleGlanceWidgetContent() 74 | } 75 | } 76 | } 77 | 78 | companion object { 79 | private val TEST_SIZE = DpSize(300.dp, 200.dp) 80 | 81 | private fun captureAndVerifyScreenshot(goldenFileName: String) { 82 | onView(ViewMatchers.isRoot()) 83 | .captureRoboImage( 84 | filePath = "src/test/resources/golden/$goldenFileName.png", 85 | roborazziOptions = RoborazziOptions( 86 | compareOptions = RoborazziOptions.CompareOptions( 87 | changeThreshold = 0F 88 | ) 89 | ) 90 | ) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /sample/src/test/resources/golden/sample_content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/test/resources/golden/sample_content.png -------------------------------------------------------------------------------- /sample/src/test/resources/golden/sample_content_rtl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/glance-experimental-tools/818e293daca936d91429da493c87550303549b57/sample/src/test/resources/golden/sample_content_rtl.png -------------------------------------------------------------------------------- /scripts/generate_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2022 The Android Open Source Project 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 | # Fail on any error 20 | set -ex 21 | 22 | DOCS_ROOT=docs-gen 23 | 24 | [ -d $DOCS_ROOT ] && rm -r $DOCS_ROOT 25 | mkdir $DOCS_ROOT 26 | 27 | # Clear out the old API docs 28 | [ -d docs/api ] && rm -r docs/api 29 | # Build the docs with dokka 30 | ./gradlew dokkaHtmlMultiModule --stacktrace 31 | 32 | # Create a copy of our docs at our $DOCS_ROOT 33 | cp -a docs/* $DOCS_ROOT 34 | 35 | cp README.md $DOCS_ROOT/index.md 36 | cp CONTRIBUTING.md $DOCS_ROOT/contributing.md 37 | 38 | sed -i.bak 's/CONTRIBUTING.md/contributing/' $DOCS_ROOT/index.md 39 | sed -i.bak 's/README.md//' $DOCS_ROOT/index.md 40 | sed -i.bak 's/docs\/header.png/header.png/' $DOCS_ROOT/index.md 41 | 42 | # Convert docs/xxx.md links to just xxx/ 43 | sed -i.bak 's/docs\/\([a-zA-Z-]*\).md/\1/' $DOCS_ROOT/index.md 44 | 45 | # Finally delete all of the backup files 46 | find . -name '*.bak' -delete 47 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright 2022 The Android Open Source Project 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 | # Fail on error and print out commands 20 | set -ex 21 | 22 | # By default we don't shard 23 | SHARD_COUNT=0 24 | SHARD_INDEX=0 25 | # By default we don't log 26 | LOG_FILE="" 27 | # By default we run tests on device 28 | DEVICE=true 29 | 30 | # Parse parameters 31 | for i in "$@"; do 32 | case $i in 33 | --shard-count=*) 34 | SHARD_COUNT="${i#*=}" 35 | shift 36 | ;; 37 | --unit-tests) 38 | DEVICE=false 39 | shift 40 | ;; 41 | --shard-index=*) 42 | SHARD_INDEX="${i#*=}" 43 | shift 44 | ;; 45 | --log-file=*) 46 | LOG_FILE="${i#*=}" 47 | shift 48 | ;; 49 | --run-affected) 50 | RUN_AFFECTED=true 51 | shift 52 | ;; 53 | --run-flaky-tests) 54 | RUN_FLAKY=true 55 | shift 56 | ;; 57 | --affected-base-ref=*) 58 | BASE_REF="${i#*=}" 59 | shift 60 | ;; 61 | *) 62 | echo "Unknown option" 63 | exit 1 64 | ;; 65 | esac 66 | done 67 | 68 | # Start logcat if we have a file to log to 69 | if [[ ! -z "$LOG_FILE" ]]; then 70 | adb logcat >$LOG_FILE & 71 | fi 72 | 73 | FILTER_OPTS="" 74 | # Filter out flaky tests if we're not set to run them 75 | if [[ -z "$RUN_FLAKY" ]]; then 76 | FILTER_OPTS="$FILTER_OPTS -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest" 77 | fi 78 | 79 | # If we're set to only run affected test, update the Gradle task 80 | if [[ ! -z "$RUN_AFFECTED" ]]; then 81 | if [ "$DEVICE" = true ]; then 82 | TASK="runAffectedAndroidTests" 83 | else 84 | TASK="runAffectedUnitTests" 85 | fi 86 | TASK="$TASK -Paffected_module_detector.enable" 87 | 88 | # If we have a base branch set, add the Gradle property 89 | if [[ ! -z "$BASE_REF" ]]; then 90 | TASK="$TASK -Paffected_base_ref=$BASE_REF" 91 | fi 92 | fi 93 | 94 | # If we don't have a task yet, use the defaults 95 | if [[ -z "$TASK" ]]; then 96 | if [ "$DEVICE" = true ]; then 97 | TASK="connectedCheck" 98 | else 99 | TASK="testDebug" 100 | fi 101 | fi 102 | 103 | SHARD_OPTS="" 104 | if [ "$SHARD_COUNT" -gt "0" ]; then 105 | # If we have a shard count value, create the necessary Gradle property args. 106 | # We assume that SHARD_INDEX has been set too 107 | SHARD_OPTS="$SHARD_OPTS -Pandroid.testInstrumentationRunnerArguments.numShards=$SHARD_COUNT" 108 | SHARD_OPTS="$SHARD_OPTS -Pandroid.testInstrumentationRunnerArguments.shardIndex=$SHARD_INDEX" 109 | fi 110 | 111 | ./gradlew --scan --continue --no-configuration-cache --stacktrace $TASK $FILTER_OPTS $SHARD_OPTS 112 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.gradle.enterprise' version '3.10.1' 19 | } 20 | 21 | gradleEnterprise { 22 | buildScan { 23 | termsOfServiceUrl = 'https://gradle.com/terms-of-service' 24 | termsOfServiceAgree = 'yes' 25 | } 26 | } 27 | 28 | include ':sample' 29 | include ':appwidget-host' 30 | include ':appwidget-viewer' 31 | include ':appwidget-configuration' 32 | include ':appwidget-testing' -------------------------------------------------------------------------------- /spotless/copyright.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright $YEAR The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /spotless/greclipse.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | #Whether to use 'space', 'tab' or 'mixed' (both) characters for indentation. 18 | #The default value is 'tab'. 19 | org.eclipse.jdt.core.formatter.tabulation.char=space 20 | 21 | #Number of spaces used for indentation in case 'space' characters 22 | #have been selected. The default value is 4. 23 | org.eclipse.jdt.core.formatter.tabulation.size=4 24 | 25 | #Number of spaces used for indentation in case 'mixed' characters 26 | #have been selected. The default value is 4. 27 | org.eclipse.jdt.core.formatter.indentation.size=4 28 | 29 | #Whether or not indentation characters are inserted into empty lines. 30 | #The default value is 'true'. 31 | org.eclipse.jdt.core.formatter.indent_empty_lines=false 32 | 33 | #Number of spaces used for multiline indentation. 34 | #The default value is 2. 35 | groovy.formatter.multiline.indentation=2 36 | 37 | #Length after which list are considered too long. These will be wrapped. 38 | #The default value is 30. 39 | groovy.formatter.longListLength=30 40 | 41 | #Whether opening braces position shall be the next line. 42 | #The default value is 'same'. 43 | groovy.formatter.braces.start=same 44 | 45 | #Whether closing braces position shall be the next line. 46 | #The default value is 'next'. 47 | groovy.formatter.braces.end=next 48 | 49 | #Remove unnecessary semicolons. The default value is 'false'. 50 | groovy.formatter.remove.unnecessary.semicolons=false --------------------------------------------------------------------------------