├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
134 |
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/AOSP.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/ktlint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/kotlinScripting.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 | [](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 super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> 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 super kotlin.Unit>);
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 super T,? extends T> 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 | [](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 super android.content.Context,? super kotlin.coroutines.Continuation super android.widget.RemoteViews>,?> 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 super kotlin.Result extends android.net.Uri>>);
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 | [](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 | [](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 | Single |
105 | Exact |
106 | Responsive |
107 |
108 |
109 |  |
110 |  |
111 |  |
112 |
113 |
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 super android.widget.RemoteViews>);
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 super android.widget.RemoteViews>);
24 | method public abstract suspend Object? getGlanceSnapshot(Class extends androidx.glance.appwidget.GlanceAppWidgetReceiver> receiver, kotlin.coroutines.Continuation super com.google.android.glance.tools.viewer.GlanceSnapshot>);
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
--------------------------------------------------------------------------------