Repository: google/accompanist
Branch: main
Commit: 12ec3408fc10
Files: 157
Total size: 515.2 KB
Directory structure:
gitextract_cgrc05nu/
├── .allstar/
│ └── binary_artifacts.yaml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── adaptive-bug-report.md
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ ├── general-bug-report.md
│ │ ├── general-other-bug-report.md
│ │ ├── navigation-material-bug-report.md
│ │ ├── permissions-bug-report.md
│ │ └── testharness-bug-report.md
│ ├── auto-merge.yml
│ ├── ci-gradle.properties
│ ├── pull_request_template.md
│ ├── release-drafter.yml
│ └── workflows/
│ ├── automerger.yml
│ ├── build-snapshot.yml
│ ├── build.yml
│ ├── issues-stale.yml
│ ├── publish-docs.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
│ ├── runConfigurations.xml
│ └── vcs.xml
├── ASSETS_LICENSE.txt
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── adaptive/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── adaptive/
│ │ ├── DisplayFeatures.kt
│ │ ├── FoldAwareColumn.kt
│ │ ├── FoldAwareColumnScope.kt
│ │ ├── RowColumnImpl.kt
│ │ ├── RowColumnMeasurementHelper.kt
│ │ └── TwoPane.kt
│ ├── sharedTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── adaptive/
│ │ ├── DisplayFeaturesTest.kt
│ │ ├── FoldAwareColumnTest.kt
│ │ └── TwoPaneTest.kt
│ └── test/
│ └── resources/
│ └── robolectric.properties
├── build-logic/
│ ├── convention/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── AndroidLibraryConventionPlugin.kt
│ │ ├── AndroidLibraryPublishedConventionPlugin.kt
│ │ ├── AndroidLintConventionPlugin.kt
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ ├── AndroidCompose.kt
│ │ ├── BundleInsideHelper.kt
│ │ ├── KotlinAndroid.kt
│ │ └── ProjectExtensions.kt
│ ├── gradle.properties
│ └── settings.gradle.kts
├── build.gradle
├── checksum.sh
├── docs/
│ ├── adaptive.md
│ ├── appcompat-theme.md
│ ├── drawablepainter.md
│ ├── migration.md
│ ├── navigation-animation.md
│ ├── navigation-material.md
│ ├── permissions.md
│ ├── systemuicontroller.md
│ ├── updating.md
│ ├── using-snapshot-version.md
│ └── web.md
├── drawablepainter/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── drawablepainter/
│ └── DrawablePainter.kt
├── generate_docs.sh
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── images/
│ └── Social.sketch
├── internal-testutils/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── internal/
│ │ └── test/
│ │ ├── ActivityScenario.kt
│ │ ├── Assertions.kt
│ │ ├── IgnoreOnRobolectric.kt
│ │ ├── TestUtils.kt
│ │ └── WaitUntil.kt
│ └── res/
│ └── values/
│ └── themes.xml
├── mkdocs.yml
├── permissions/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ ├── androidTest/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── permissions/
│ │ ├── FakeTests.kt
│ │ ├── MultipleAndSinglePermissionsTest.kt
│ │ ├── MultiplePermissionsStateTest.kt
│ │ ├── PermissionStateTest.kt
│ │ ├── RequestMultiplePermissionsTest.kt
│ │ ├── RequestPermissionTest.kt
│ │ ├── TestUtils.kt
│ │ └── test/
│ │ ├── EmptyPermissionsTestActivity.kt
│ │ └── PermissionsTestActivity.kt
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── permissions/
│ ├── MultiplePermissionsState.kt
│ ├── MutableMultiplePermissionsState.kt
│ ├── MutablePermissionState.kt
│ ├── PermissionState.kt
│ └── PermissionsUtil.kt
├── permissions-lint/
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── google/
│ │ │ └── accompanist/
│ │ │ └── permissions/
│ │ │ └── lint/
│ │ │ ├── PermissionsIssueRegistry.kt
│ │ │ ├── PermissionsLaunchDetector.kt
│ │ │ └── util/
│ │ │ ├── ComposableUtils.kt
│ │ │ ├── KotlinMetadataUtils.kt
│ │ │ ├── Names.kt
│ │ │ └── PsiUtils.kt
│ │ └── resources/
│ │ └── META-INF/
│ │ └── services/
│ │ └── com.android.tools.lint.client.api.IssueRegistry
│ └── test/
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── permissions/
│ └── lint/
│ └── PermissionsLaunchDetectorTest.kt
├── release/
│ ├── secring.gpg.aes
│ ├── signing-cleanup.sh
│ ├── signing-setup.sh
│ └── signing.properties.aes
├── sample/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── sample/
│ │ ├── ImageLoadingSampleUtils.kt
│ │ ├── MainActivity.kt
│ │ ├── MainScreen.kt
│ │ ├── Theme.kt
│ │ ├── adaptive/
│ │ │ ├── BasicTwoPaneSample.kt
│ │ │ ├── DraggableFoldAwareColumnSample.kt
│ │ │ ├── HorizontalTwoPaneSample.kt
│ │ │ ├── NavDrawerFoldAwareColumnSample.kt
│ │ │ ├── NavRailFoldAwareColumnSample.kt
│ │ │ └── VerticalTwoPaneSample.kt
│ │ ├── drawablepainter/
│ │ │ └── DocSamples.kt
│ │ └── permissions/
│ │ ├── RequestLocationPermissionsSample.kt
│ │ ├── RequestMultiplePermissionsSample.kt
│ │ └── RequestPermissionSample.kt
│ └── res/
│ ├── drawable/
│ │ ├── ic_launcher_background.xml
│ │ └── rectangle.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── values/
│ │ └── strings.xml
│ └── values-ar/
│ └── strings.xml
├── scripts/
│ └── run-tests.sh
├── settings.gradle.kts
└── spotless/
├── copyright.txt
└── greclipse.properties
================================================
FILE CONTENTS
================================================
================================================
FILE: .allstar/binary_artifacts.yaml
================================================
# Exemption reason: This repo uses binary artifacts to ship gradle.jar for users. It does not allow any others.
# Exemption timeframe: permanent
optConfig:
optOut: true
================================================
FILE: .github/ISSUE_TEMPLATE/adaptive-bug-report.md
================================================
---
name: Adaptive bug report
about: Create a report about adaptive
title: "[Adaptive]"
labels: adaptive
assignees: alexvanyo
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior
A clear and concise description of what you expected to happen.
## Screenshots?
If applicable, add screenshots to help explain your problem.
## Environment:
- Android OS version: [e.g. Android 5.0]
- Device: [e.g. Emulator, Google Pixel 4]
- Accompanist version: [e.g. v0.X]
## Additional context
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/general-bug-report.md
================================================
---
name: General Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce (if applicable)
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior (if applicable)
A clear and concise description of what you expected to happen.
## Screenshots? (if applicable)
If applicable, add screenshots to help explain your problem.
## Environment: (if applicable)
- Android OS version: [e.g. Android 5.0]
- Device: [e.g. Emulator, Google Pixel 4]
- Accompanist version: [e.g. v0.X]
## Additional context
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/general-other-bug-report.md
================================================
---
name: General/Other bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce (if applicable)
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior (if applicable)
A clear and concise description of what you expected to happen.
## Screenshots? (if applicable)
If applicable, add screenshots to help explain your problem.
## Environment: (if applicable)
- Android OS version: [e.g. Android 5.0]
- Device: [e.g. Emulator, Google Pixel 4]
- Accompanist version: [e.g. v0.X]
## Additional context
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/navigation-material-bug-report.md
================================================
---
name: Navigation Material bug report
about: Create a report to help us improve
title: "[Navigation Material] "
labels: ''
assignees: jossiwolf
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/ISSUE_TEMPLATE/permissions-bug-report.md
================================================
---
name: Permissions bug report
about: Create a report to help us improve
title: "[Permissions] "
labels: ''
assignees: bentrengrove
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/ISSUE_TEMPLATE/testharness-bug-report.md
================================================
---
name: Test harness bug report
about: Create a report about test harness
title: "[Test Harness]"
labels: testharness
assignees: alexvanyo
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/auto-merge.yml
================================================
# Config for github.com/bobvanderlinden/probot-auto-merge
minApprovals:
COLLABORATOR: 1
requiredLabels:
- automerge
mergeMethod: merge
reportStatus: true
================================================
FILE: .github/ci-gradle.properties
================================================
#
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Turn Gradle daemon off due to https://github.com/Kotlin/dokka/issues/1405
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.workers.max=2
kotlin.compiler.execution.strategy=in-process
================================================
FILE: .github/pull_request_template.md
================================================
### Please add the library name to the PR title. Example: "[Insets] Fixes typo" ###
================================================
FILE: .github/release-drafter.yml
================================================
name-template: 'v$NEXT_PATCH_VERSION 🌈'
tag-template: 'v$NEXT_PATCH_VERSION'
template: |
## What’s Changed
$CHANGES
================================================
FILE: .github/workflows/automerger.yml
================================================
name: main to snapshot auto merger
on:
push:
branches:
- main
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: '0' # 0 == fetch all history history
ref: 'snapshot'
token: ${{ secrets.AUTOMERGE_PAT }}
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git fetch origin
git merge origin/main --no-edit
git push
================================================
FILE: .github/workflows/build-snapshot.yml
================================================
name: Build & test (snapshot)
on:
push:
branches:
- snapshot
paths-ignore:
- '**.md'
pull_request:
branches:
- snapshot
workflow_dispatch:
jobs:
build:
# Skip build if head commit contains 'skip ci'
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v2
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: set up JDK
uses: actions/setup-java@v1
with:
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}
- name: Build
run: |
./gradlew --scan --stacktrace \
spotlessCheck \
assemble \
metalavaCheckCompatibilityRelease \
lintDebug
- name: Unit Tests
run: |
./scripts/run-tests.sh \
--unit-tests \
--run-affected \
--affected-base-ref=$BASE_REF
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-robolectric
path: |
**/build/test-results/*
**/build/reports/*
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
test:
runs-on: macos-latest
needs: build
timeout-minutes: 50
strategy:
# Allow tests to continue on other devices if they fail on one device.
fail-fast: false
matrix:
api-level: [ 22, 26, 29, 31, 32 ]
shard: [ 0, 1 ] # Need to update shard-count below if this changes
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: set up JDK
uses: actions/setup-java@v1
with:
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}
# Determine what emulator image to use. We run all API 28+ emulators using
# the google_apis image
- name: Determine emulator target
id: determine-target
env:
API_LEVEL: ${{ matrix.api-level }}
run: |
TARGET="default"
if [ "$API_LEVEL" -ge "28" ]; then
TARGET="google_apis"
fi
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ steps.determine-target.outputs.TARGET }}
profile: Galaxy Nexus
script: ./scripts/run-tests.sh --log-file=logcat.txt --run-affected --affected-base-ref=$BASE_REF --shard-index=${{ matrix.shard }} --shard-count=2
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
- name: Upload logs
if: always()
uses: actions/upload-artifact@v2
with:
name: logs-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: logcat.txt
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: test-results-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: |
**/build/reports/*
**/build/outputs/*/connected/*
deploy:
if: github.event_name == 'push' # only deploy for pushed commits (not PRs)
runs-on: ubuntu-latest
needs: [ build, test ]
timeout-minutes: 30
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: set up JDK
uses: actions/setup-java@v1
with:
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}
- name: Deploy to Sonatype
run: ./gradlew publish --no-parallel --stacktrace --no-configuration-cache
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
================================================
FILE: .github/workflows/build.yml
================================================
name: Build & test
on:
push:
branches:
- main
- compose-1.0
- compose-1.1
- compose-1.2
- compose-1.3
- compose-1.4
- compose-1.5
- compose-1.6
paths-ignore:
- '**.md'
pull_request:
jobs:
build:
# Skip build if head commit contains 'skip ci'
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v2
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Build
run: |
./gradlew --scan --stacktrace \
spotlessCheck \
assemble \
metalavaCheckCompatibilityRelease \
lintDebug
- name: Unit Tests
run: |
./scripts/run-tests.sh \
--unit-tests \
--run-affected \
--affected-base-ref=$BASE_REF
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-robolectric
path: |
**/build/test-results/*
**/build/reports/*
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
test:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 70
strategy:
# Allow tests to continue on other devices if they fail on one device.
fail-fast: false
matrix:
api-level: [ 22, 26, 30 ]
shard: [ 0, 1 ] # Need to update shard-count below if this changes
env:
TERM: dumb
steps:
- uses: actions/checkout@v3
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
# Determine what emulator image to use. We run all API 28+ emulators using
# the google_apis image
- name: Determine emulator target
id: determine-target
env:
API_LEVEL: ${{ matrix.api-level }}
run: |
TARGET="default"
if [ "$API_LEVEL" -ge "28" ]; then
TARGET="google_apis"
fi
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
- name: Determine emulator arch
id: determine-arch
env:
API_LEVEL: ${{ matrix.api-level }}
run: |
ARCH="x86"
if [ "$API_LEVEL" -ge "29" ]; then
ARCH="x86_64"
fi
echo "ARCH=$ARCH" >> $GITHUB_OUTPUT
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: ${{ steps.determine-arch.outputs.ARCH }}
target: ${{ steps.determine-target.outputs.TARGET }}
profile: Galaxy Nexus
script: ./scripts/run-tests.sh --log-file=logcat.txt --run-affected --affected-base-ref=$BASE_REF --shard-index=${{ matrix.shard }} --shard-count=2
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: logcat.txt
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: |
**/build/reports/*
**/build/outputs/*/connected/*
deploy:
if: github.event_name == 'push' # only deploy for pushed commits (not PRs)
runs-on: ubuntu-latest
needs: [ build, test ]
timeout-minutes: 30
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Deploy to Sonatype
run: ./gradlew publish --no-parallel --stacktrace --no-configuration-cache
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
================================================
FILE: .github/workflows/issues-stale.yml
================================================
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '15 3 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
days-before-stale: 45
days-before-close: 5
exempt-all-pr-milestones: true
exempt-issue-labels: 'waiting for info,waiting on dependency'
================================================
FILE: .github/workflows/publish-docs.yml
================================================
name: Publish docs
on:
push:
tags:
- v*
workflow_dispatch:
jobs:
deploy_docs:
runs-on: ubuntu-latest
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install mkdocs-material=="9.*"
- name: Generate docs
run: ./generate_docs.sh
- name: Build site
run: mkdocs build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
================================================
FILE: .github/workflows/update-release.yml
================================================
name: Update release
on:
push:
branches:
- main
jobs:
update_draft_release:
runs-on: ubuntu-latest
steps:
# pin directly to 6.0.0 because we don't want to update without knowledge
- uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# Gradle
.gradle
build/
captures
/local.properties
# IntelliJ .idea folder
.idea/workspace.xml
.idea/libraries
.idea/caches
.idea/navEditor.xml
.idea/tasks.xml
.idea/modules.xml
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/deploymentTargetDropDown.xml
.idea/misc.xml
.idea/androidTestResultsUserPreferences.xml
.idea/deploymentTargetSelector.xml
gradle.xml
*.iml
# General
.DS_Store
.externalNativeBuild
# Do not commit plain-text signing info
release/*.properties
release/*.gpg
# VS Code config
org.eclipse.buildship.core.prefs
.classpath
.project
# Temporary API docs
docs/api
package-list-coil-base
# Mkdocs temporary serving folder
docs-gen
site
*.bak
# Lint reports
lint-report.*
================================================
FILE: .idea/codeStyles/Project.xml
================================================
.*:id
http://schemas.android.com/apk/res/android
.*:name
http://schemas.android.com/apk/res/android
.*
http://schemas.android.com/apk/res/android
ANDROID_ATTRIBUTE_ORDER
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
================================================
FILE: .idea/copyright/AOSP.xml
================================================
================================================
FILE: .idea/copyright/profiles_settings.xml
================================================
================================================
FILE: .idea/inspectionProfiles/ktlint.xml
================================================
================================================
FILE: .idea/inspectionProfiles/profiles_settings.xml
================================================
================================================
FILE: .idea/kotlinScripting.xml
================================================
================================================
FILE: .idea/kotlinc.xml
================================================
================================================
FILE: .idea/runConfigurations.xml
================================================
================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: ASSETS_LICENSE.txt
================================================
All font files are licensed under the SIL OPEN FONT LICENSE license. All other files are licensed under the Apache 2 license.
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## New Features/Libraries
Before contributing large new features and/or libraries please start a discussion
with us first via GitHub Issues and check that we can support it.
We are unable to support all new features, even though we wish we could! If we
are unable to support adding your feature, we always encourage you to open source it
in your own repository to help the Compose community grow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code Reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## API Changes
If you are changing any public APIs, you need to run `./gradlew metalavaGenerateSignatureRelease` which will
update the API signatures.
## Formatting
To apply formatting, we use spotless. Run `./gradlew :pager:spotlessApply` to format the code according
to the spec.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================

Accompanist is a group of libraries that aim to supplement [Jetpack Compose][compose] with features that are commonly required by developers but not yet available.
Accompanist is a labs like environment for new Compose APIs. We use it to help fill known gaps in the Compose toolkit, experiment with new APIs and to gather insight into the development experience of developing a Compose library. The goal of these libraries is to upstream them into the official toolkit, at which point they will be deprecated and removed from Accompanist.
For more details like, why does this library exist? Why is it not part of AndroidX? Will you be releasing more libraries? Check out our [Accompanist FAQ](https://medium.com/p/b55117b02712).
## Compose versions
Each [release](https://github.com/google/accompanist/releases) outlines what version of the Compose UI libraries it depends on. We are currently releasing multiple versions of Accompanist for the different versions of Compose:
Compose 1.0 (1.0.x)
Compose 1.1 (1.1.x)
Compose UI 1.2 (1.2.x)
Compose UI 1.3 (1.3.x)
Compose UI 1.4 (1.4.x)
Compose UI 1.5 (1.5.x)
Compose UI 1.6 (1.6.x)
Compose UI 1.7+ (1.7.x)
For stable versions of Compose, we use the latest *stable* version of the Compose compiler. For non-stable versions (alpha, beta, etc), we use the latest compiler at the time of release.
> :warning: **Ensure you are using the Accompanist version that matches with your Compose UI version**: If you upgrade Accompanist, it will upgrade your Compose libraries version via transitive dependencies.
## Libraries
### 📫 [Permissions](./permissions/)
A library that provides [Android runtime permissions][runtimepermissions] support for Jetpack Compose.
### 🖌️ [Drawable Painter](./drawablepainter/)
A library which provides a way to use Android Drawables as Jetpack Compose Painters.
### 📜 [Adaptive](./adaptive/)
A library providing a collection of utilities for adaptive layouts.
### 🧭✨[Navigation-Animation](./navigation-animation/) (Deprecated & Removed)
See our [Migration Guide](https://google.github.io/accompanist/navigation-animation/) for migrating to using built in support for animations in Jetpack Navigation Compose.
### 🧭🎨️ [Navigation-Material](./navigation-material/) (Deprecated & Removed)
See our [Migration Guide](https://google.github.io/accompanist/navigation-material/) for migrating to using built in material-navigation support.
### 🍫 [System UI Controller](./systemuicontroller/) (Deprecated & Removed)
We recommend migrating to edge to edge. See our [Migration Guide](https://google.github.io/accompanist/systemuicontroller/) for more details.
---
## Future?
Any of the features available in this group of libraries may become obsolete in the future, at which point they will (probably) become deprecated.
We will aim to provide a migration path (where possible), to whatever supersedes the functionality.
## Snapshots
Snapshots of the current development version of Accompanist are available, which track the latest commit. See [here](docs/using-snapshot-version.md) for more information.
---
### Why the name?
The library is all about adding some utilities around Compose. Music composing is done by a
composer, and since this library is about supporting composition, the supporting role of an [accompanist](https://en.wikipedia.org/wiki/Accompaniment) felt like a good name.
## Contributions
Please contribute! We will gladly review any pull requests.
Make sure to read the [Contributing](CONTRIBUTING.md) page first though.
## License
```
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
[appcompat]: https://developer.android.com/jetpack/androidx/releases/appcompat
[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/
[mdc]: https://github.com/material-components/material-components-android
[windowinsets]: https://developer.android.com/reference/kotlin/android/view/WindowInsets
[viewpager]: https://developer.android.com/reference/kotlin/androidx/viewpager/widget/ViewPager
[runtimepermissions]: https://developer.android.com/guide/topics/permissions/overview
================================================
FILE: adaptive/README.md
================================================
# Adaptive utilities for Jetpack Compose
[](https://search.maven.org/search?q=g:com.google.accompanist)
For more information, visit the documentation: https://google.github.io/accompanist/adaptive
## Download
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-adaptive:"
}
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-adaptive/
================================================
FILE: adaptive/api/current.api
================================================
// Signature format: 4.0
package com.google.accompanist.adaptive {
public final class DisplayFeaturesKt {
method @androidx.compose.runtime.Composable public static java.util.List calculateDisplayFeatures(android.app.Activity activity);
}
public final class FoldAwareColumnKt {
method @androidx.compose.runtime.Composable public static void FoldAwareColumn(java.util.List extends androidx.window.layout.DisplayFeature> displayFeatures, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues foldPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1 super com.google.accompanist.adaptive.FoldAwareColumnScope,kotlin.Unit> content);
}
@androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FoldAwareColumnScope {
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier align(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Horizontal alignment);
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, androidx.compose.ui.layout.VerticalAlignmentLine alignmentLine);
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1 super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier ignoreFold(androidx.compose.ui.Modifier);
}
@kotlin.jvm.JvmInline public final value class FoldAwareConfiguration {
field public static final com.google.accompanist.adaptive.FoldAwareConfiguration.Companion Companion;
}
public static final class FoldAwareConfiguration.Companion {
method public int getAllFolds();
method public int getHorizontalFoldsOnly();
method public int getVerticalFoldsOnly();
property public final int AllFolds;
property public final int HorizontalFoldsOnly;
property public final int VerticalFoldsOnly;
}
public final class SplitResult {
ctor public SplitResult(androidx.compose.foundation.gestures.Orientation gapOrientation, androidx.compose.ui.geometry.Rect gapBounds);
method public androidx.compose.ui.geometry.Rect getGapBounds();
method public androidx.compose.foundation.gestures.Orientation getGapOrientation();
property public final androidx.compose.ui.geometry.Rect gapBounds;
property public final androidx.compose.foundation.gestures.Orientation gapOrientation;
}
public final class TwoPaneKt {
method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(float splitOffset, optional boolean offsetFromStart, optional float gapWidth);
method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(float splitFraction, optional float gapWidth);
method @androidx.compose.runtime.Composable public static void TwoPane(kotlin.jvm.functions.Function0 first, kotlin.jvm.functions.Function0 second, com.google.accompanist.adaptive.TwoPaneStrategy strategy, java.util.List extends androidx.window.layout.DisplayFeature> displayFeatures, optional androidx.compose.ui.Modifier modifier, optional int foldAwareConfiguration);
method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(float splitOffset, optional boolean offsetFromTop, optional float gapHeight);
method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(float splitFraction, optional float gapHeight);
}
public fun interface TwoPaneStrategy {
method public com.google.accompanist.adaptive.SplitResult calculateSplitResult(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates);
}
}
================================================
FILE: adaptive/build.gradle.kts
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
plugins {
alias(libs.plugins.accompanist.android.library)
alias(libs.plugins.accompanist.android.library.compose)
alias(libs.plugins.accompanist.android.library.published)
}
android {
namespace = "com.google.accompanist.adaptive"
sourceSets {
named("test") {
java.srcDirs("src/sharedTest/kotlin")
res.srcDirs("src/sharedTest/res")
}
named("androidTest") {
java.srcDirs("src/sharedTest/kotlin")
res.srcDirs("src/sharedTest/res")
}
}
}
dependencies {
api(libs.compose.foundation.foundation)
api(libs.compose.ui.ui)
api(libs.androidx.window)
implementation(libs.kotlin.coroutines.android)
implementation(libs.compose.ui.util)
// ======================
// Test dependencies
// ======================
androidTestImplementation(project(":internal-testutils"))
testImplementation(project(":internal-testutils"))
androidTestImplementation(libs.junit)
testImplementation(libs.junit)
androidTestImplementation(libs.truth)
testImplementation(libs.truth)
androidTestImplementation(libs.compose.ui.test.junit4)
testImplementation(libs.compose.ui.test.junit4)
androidTestImplementation(libs.compose.ui.test.manifest)
testImplementation(libs.compose.ui.test.manifest)
androidTestImplementation(libs.androidx.test.runner)
testImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.window.testing)
testImplementation(libs.androidx.window.testing)
testImplementation(libs.robolectric)
}
================================================
FILE: adaptive/gradle.properties
================================================
POM_ARTIFACT_ID=accompanist-adaptive
POM_NAME=Accompanist Adaptive library
POM_PACKAGING=aar
================================================
FILE: adaptive/src/main/AndroidManifest.xml
================================================
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/DisplayFeatures.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.window.layout.DisplayFeature
import androidx.window.layout.WindowInfoTracker
/**
* Calculates the list of [DisplayFeature]s from the given [activity].
*/
@Composable
public fun calculateDisplayFeatures(activity: Activity): List {
val windowLayoutInfo = remember(activity) {
WindowInfoTracker.getOrCreate(activity).windowLayoutInfo(activity)
}
val displayFeatures by produceState(
initialValue = emptyList(),
key1 = windowLayoutInfo
) {
windowLayoutInfo.collect { info ->
value = info.displayFeatures
}
}
return displayFeatures
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/FoldAwareColumn.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import android.util.Range
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import kotlin.math.roundToInt
/**
* A simplified version of [Column] that places children in a fold-aware manner.
*
* The layout starts placing children from the top of the available space. If there is a horizontal
* [separating](https://developer.android.com/reference/kotlin/androidx/window/layout/FoldingFeature#isSeparating())
* fold present in the window, then the layout will check to see if any children overlap the fold.
* If a child would overlap the fold in its current position, then the layout will increase its
* y coordinate so that the child is now placed below the fold, and any subsequent children will
* also be placed below the fold.
*
*
* @param displayFeatures a list of display features the device currently has
* @param modifier an optional modifier for the layout
* @param foldPadding the optional padding to add around a fold
* @param horizontalAlignment the horizontal alignment of the layout's children.
*/
@Composable
public fun FoldAwareColumn(
displayFeatures: List,
modifier: Modifier = Modifier,
foldPadding: PaddingValues = PaddingValues(),
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable FoldAwareColumnScope.() -> Unit,
) {
Layout(
modifier = modifier,
measurePolicy = foldAwareColumnMeasurePolicy(
verticalArrangement = Arrangement.Top,
horizontalAlignment = horizontalAlignment,
fold = {
// Extract folding feature if horizontal and separating
displayFeatures.find {
it is FoldingFeature && it.orientation == FoldingFeature.Orientation.HORIZONTAL &&
it.isSeparating
} as FoldingFeature?
},
foldPadding = foldPadding,
),
content = { FoldAwareColumnScopeInstance.content() }
)
}
/**
* FoldAwareColumn version of [rowColumnMeasurePolicy] that uses [FoldAwareColumnMeasurementHelper.foldAwarePlaceHelper]
* method instead of [RowColumnMeasurementHelper.placeHelper]
*/
// TODO: change from internal to private once metalava issue is solved https://issuetracker.google.com/issues/271539608
@Composable
internal fun foldAwareColumnMeasurePolicy(
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
fold: () -> FoldingFeature?,
foldPadding: PaddingValues
) = remember(verticalArrangement, horizontalAlignment, fold, foldPadding) {
val orientation = LayoutOrientation.Vertical
val arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit =
{ totalSize, size, _, density, outPosition ->
with(verticalArrangement) { density.arrange(totalSize, size, outPosition) }
}
val arrangementSpacing = verticalArrangement.spacing
val crossAxisAlignment = CrossAxisAlignment.horizontal(horizontalAlignment)
val crossAxisSize = SizeMode.Wrap
object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List,
constraints: Constraints
): MeasureResult {
val placeables = arrayOfNulls(measurables.size)
val rowColumnMeasureHelper =
FoldAwareColumnMeasurementHelper(
orientation,
arrangement,
arrangementSpacing,
crossAxisSize,
crossAxisAlignment,
measurables,
placeables
)
val measureResult = rowColumnMeasureHelper
.measureWithoutPlacing(
this,
constraints, 0, measurables.size
)
val layoutWidth: Int
val layoutHeight: Int
if (orientation == LayoutOrientation.Horizontal) {
layoutWidth = measureResult.mainAxisSize
layoutHeight = measureResult.crossAxisSize
} else {
layoutWidth = measureResult.crossAxisSize
layoutHeight = measureResult.mainAxisSize
}
// Calculate fold bounds in pixels (including any added fold padding)
val foldBoundsPx = with(density) {
val topPaddingPx = foldPadding.calculateTopPadding().roundToPx()
val bottomPaddingPx = foldPadding.calculateBottomPadding().roundToPx()
fold()?.bounds?.let {
Rect(
left = it.left.toFloat(),
top = it.top.toFloat() - topPaddingPx,
right = it.right.toFloat(),
bottom = it.bottom.toFloat() + bottomPaddingPx
)
}
}
// We only know how much padding is added inside the placement scope, so just add fold height
// and height of the largest child when laying out to cover the maximum possible height
val heightPadding = foldBoundsPx?.let { bounds ->
val largestChildHeight = rowColumnMeasureHelper.placeables.maxOfOrNull {
if ((it?.parentData as? RowColumnParentData)?.ignoreFold == true) {
0
} else {
it?.height ?: 0
}
} ?: 0
bounds.height.roundToInt() + largestChildHeight
} ?: 0
val paddedLayoutHeight = layoutHeight + heightPadding
return layout(layoutWidth, paddedLayoutHeight) {
rowColumnMeasureHelper.foldAwarePlaceHelper(
this,
measureResult,
0,
layoutDirection,
foldBoundsPx
)
}
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List,
height: Int
) = MinIntrinsicWidthMeasureBlock(orientation)(
measurables,
height,
arrangementSpacing.roundToPx()
)
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List,
width: Int
) = MinIntrinsicHeightMeasureBlock(orientation)(
measurables,
width,
arrangementSpacing.roundToPx()
)
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List,
height: Int
) = MaxIntrinsicWidthMeasureBlock(orientation)(
measurables,
height,
arrangementSpacing.roundToPx()
)
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List,
width: Int
) = MaxIntrinsicHeightMeasureBlock(orientation)(
measurables,
width,
arrangementSpacing.roundToPx()
)
}
}
/**
* Inherits from [RowColumnMeasurementHelper] to place children in a fold-aware manner
*/
private class FoldAwareColumnMeasurementHelper(
orientation: LayoutOrientation,
arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
arrangementSpacing: Dp,
crossAxisSize: SizeMode,
crossAxisAlignment: CrossAxisAlignment,
measurables: List,
placeables: Array
) : RowColumnMeasurementHelper(
orientation,
arrangement,
arrangementSpacing,
crossAxisSize,
crossAxisAlignment,
measurables,
placeables
) {
/**
* Copy of [placeHelper] that has been modified for FoldAwareColumn implementation
*/
@OptIn(ExperimentalComposeUiApi::class)
fun foldAwarePlaceHelper(
placeableScope: Placeable.PlacementScope,
measureResult: RowColumnMeasureHelperResult,
crossAxisOffset: Int,
layoutDirection: LayoutDirection,
foldBoundsPx: Rect?
) {
with(placeableScope) {
val layoutBounds = coordinates!!.trueBoundsInWindow()
var placeableY = 0
for (i in measureResult.startIndex until measureResult.endIndex) {
val placeable = placeables[i]!!
val mainAxisPositions = measureResult.mainAxisPositions
val crossAxisPosition = getCrossAxisPosition(
placeable,
(measurables[i].parentData as? RowColumnParentData),
measureResult.crossAxisSize,
layoutDirection,
measureResult.beforeCrossAxisAlignmentLine
) + crossAxisOffset
if (orientation == LayoutOrientation.Horizontal) {
placeable.place(
mainAxisPositions[i - measureResult.startIndex],
crossAxisPosition
)
} else {
val relativeBounds = Rect(
left = 0f,
top = placeableY.toFloat(),
right = placeable.width.toFloat(),
bottom = (placeableY + placeable.height).toFloat()
)
val absoluteBounds =
relativeBounds.translate(layoutBounds.left, layoutBounds.top)
// If placeable overlaps fold, push placeable below
if (foldBoundsPx?.overlapsVertically(absoluteBounds) == true &&
(placeable.parentData as? RowColumnParentData)?.ignoreFold != true
) {
placeableY = (foldBoundsPx.bottom - layoutBounds.top).toInt()
}
placeable.place(crossAxisPosition, placeableY)
placeableY += placeable.height
}
}
}
}
}
/**
* Copy of original [LayoutCoordinates.boundsInWindow], but without the nonzero dimension check.
*
* Instead of returning [Rect.Zero] for a layout with zero width/height, this method will still
* return a Rect with the layout's bounds.
*/
@VisibleForTesting
internal fun LayoutCoordinates.trueBoundsInWindow(): Rect {
val root = findRootCoordinates()
val bounds = boundsInRoot()
val rootWidth = root.size.width.toFloat()
val rootHeight = root.size.height.toFloat()
val boundsLeft = bounds.left.coerceIn(0f, rootWidth)
val boundsTop = bounds.top.coerceIn(0f, rootHeight)
val boundsRight = bounds.right.coerceIn(0f, rootWidth)
val boundsBottom = bounds.bottom.coerceIn(0f, rootHeight)
val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))
val left = minOf(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
val top = minOf(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
val right = maxOf(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
val bottom = maxOf(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
return Rect(left, top, right, bottom)
}
/**
* Checks if the vertical ranges of the two Rects overlap (inclusive)
*/
private fun Rect.overlapsVertically(other: Rect): Boolean {
val verticalRange = Range(top, bottom)
val otherVerticalRange = Range(other.top, other.bottom)
return verticalRange.overlaps(otherVerticalRange)
}
/**
* Inclusive check to see if the given float ranges overlap
*/
private fun Range.overlaps(other: Range): Boolean {
return (lower >= other.lower && lower <= other.upper) || (upper >= other.lower && upper <= other.upper)
}
/**
* Copy of [RowColumnParentData] that has been modified to include the new ignoreFold field.
*/
internal data class RowColumnParentData(
var weight: Float = 0f,
var fill: Boolean = true,
var crossAxisAlignment: CrossAxisAlignment? = null,
var ignoreFold: Boolean = false
)
internal class IgnoreFoldModifier(
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?) =
((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.ignoreFold = true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is IgnoreFoldModifier
}
override fun hashCode(): Int {
return 0
}
override fun toString(): String =
"IgnoreFoldModifier(ignoreFold=true)"
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/FoldAwareColumnScope.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Measured
import androidx.compose.ui.layout.VerticalAlignmentLine
import androidx.compose.ui.platform.debugInspectorInfo
/**
* Copy of [ColumnScope] that excludes the weight Modifier attribute.
*
* Also adds a new [ignoreFold] Modifier attribute.
*/
@LayoutScopeMarker
@Immutable
public interface FoldAwareColumnScope {
/**
* Ignore the fold when placing this child within the [FoldAwareColumn].
*/
@Stable
public fun Modifier.ignoreFold(): Modifier
/**
* Align the element horizontally within the [Column]. This alignment will have priority over
* the [Column]'s `horizontalAlignment` parameter.
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.SimpleAlignInColumn
*/
@Stable
public fun Modifier.align(alignment: Alignment.Horizontal): Modifier
/**
* Position the element horizontally such that its [alignmentLine] aligns with sibling elements
* also configured to [alignBy]. [alignBy] is a form of [align],
* so both modifiers will not work together if specified for the same layout.
* Within a [Column], all components with [alignBy] will align horizontally using
* the specified [VerticalAlignmentLine]s or values provided using the other
* [alignBy] overload, forming a sibling group.
* At least one element of the sibling group will be placed as it had [Alignment.Start] align
* in [Column], and the alignment of the other siblings will be then determined such that
* the alignment lines coincide. Note that if only one element in a [Column] has the
* [alignBy] modifier specified the element will be positioned
* as if it had [Alignment.Start] align.
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.SimpleRelativeToSiblingsInColumn
*/
@Stable
public fun Modifier.alignBy(alignmentLine: VerticalAlignmentLine): Modifier
/**
* Position the element horizontally such that the alignment line for the content as
* determined by [alignmentLineBlock] aligns with sibling elements also configured to
* [alignBy]. [alignBy] is a form of [align], so both modifiers
* will not work together if specified for the same layout.
* Within a [Column], all components with [alignBy] will align horizontally using
* the specified [VerticalAlignmentLine]s or values obtained from [alignmentLineBlock],
* forming a sibling group.
* At least one element of the sibling group will be placed as it had [Alignment.Start] align
* in [Column], and the alignment of the other siblings will be then determined such that
* the alignment lines coincide. Note that if only one element in a [Column] has the
* [alignBy] modifier specified the element will be positioned
* as if it had [Alignment.Start] align.
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.SimpleRelativeToSiblings
*/
@Stable
public fun Modifier.alignBy(alignmentLineBlock: (Measured) -> Int): Modifier
}
internal object FoldAwareColumnScopeInstance : FoldAwareColumnScope {
@Stable
override fun Modifier.ignoreFold() = this.then(
IgnoreFoldModifier(
inspectorInfo = debugInspectorInfo {
name = "ignoreFold"
value = true
}
)
)
@Stable
override fun Modifier.align(alignment: Alignment.Horizontal) = this.then(
HorizontalAlignModifier(
horizontal = alignment,
inspectorInfo = debugInspectorInfo {
name = "align"
value = alignment
}
)
)
@Stable
override fun Modifier.alignBy(alignmentLine: VerticalAlignmentLine) = this.then(
SiblingsAlignedModifier.WithAlignmentLine(
alignmentLine = alignmentLine,
inspectorInfo = debugInspectorInfo {
name = "alignBy"
value = alignmentLine
}
)
)
@Stable
override fun Modifier.alignBy(alignmentLineBlock: (Measured) -> Int) = this.then(
SiblingsAlignedModifier.WithAlignmentLineBlock(
block = alignmentLineBlock,
inspectorInfo = debugInspectorInfo {
name = "alignBy"
value = alignmentLineBlock
}
)
)
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/RowColumnImpl.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.Measured
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
import com.google.accompanist.adaptive.LayoutOrientation.Horizontal
import com.google.accompanist.adaptive.LayoutOrientation.Vertical
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Copied from:
* RowColumnImpl.kt
* https://android-review.googlesource.com/c/platform/frameworks/support/+/2260390/27/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
*
* The only changes were updating access modifiers and removing unused code
*/
/**
* [Row] will be [Horizontal], [Column] is [Vertical].
*/
internal enum class LayoutOrientation {
Horizontal,
Vertical
}
/**
* Used to specify the alignment of a layout's children, in cross axis direction.
*/
@Immutable
internal sealed class CrossAxisAlignment {
/**
* Aligns to [size]. If this is a vertical alignment, [layoutDirection] should be
* [LayoutDirection.Ltr].
*
* @param size The remaining space (total size - content size) in the container.
* @param layoutDirection The layout direction of the content if horizontal or
* [LayoutDirection.Ltr] if vertical.
* @param placeable The item being aligned.
* @param beforeCrossAxisAlignmentLine The space before the cross-axis alignment line if
* an alignment line is being used or 0 if no alignment line is being used.
*/
internal abstract fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int
/**
* Returns `true` if this is [Relative].
*/
internal open val isRelative: Boolean
get() = false
/**
* Returns the alignment line position relative to the left/top of the space or `null` if
* this alignment doesn't rely on alignment lines.
*/
internal open fun calculateAlignmentLinePosition(placeable: Placeable): Int? = null
companion object {
/**
* Place children such that their center is in the middle of the cross axis.
*/
@Stable
val Center: CrossAxisAlignment = CenterCrossAxisAlignment
/**
* Place children such that their start edge is aligned to the start edge of the cross
* axis. TODO(popam): Consider rtl directionality.
*/
@Stable
val Start: CrossAxisAlignment = StartCrossAxisAlignment
/**
* Place children such that their end edge is aligned to the end edge of the cross
* axis. TODO(popam): Consider rtl directionality.
*/
@Stable
val End: CrossAxisAlignment = EndCrossAxisAlignment
/**
* Align children by their baseline.
*/
fun AlignmentLine(alignmentLine: AlignmentLine): CrossAxisAlignment =
AlignmentLineCrossAxisAlignment(AlignmentLineProvider.Value(alignmentLine))
/**
* Align children relative to their siblings using the alignment line provided as a
* parameter using [AlignmentLineProvider].
*/
internal fun Relative(alignmentLineProvider: AlignmentLineProvider): CrossAxisAlignment =
AlignmentLineCrossAxisAlignment(alignmentLineProvider)
/**
* Align children with vertical alignment.
*/
internal fun vertical(vertical: Alignment.Vertical): CrossAxisAlignment =
VerticalCrossAxisAlignment(vertical)
/**
* Align children with horizontal alignment.
*/
internal fun horizontal(horizontal: Alignment.Horizontal): CrossAxisAlignment =
HorizontalCrossAxisAlignment(horizontal)
}
private object CenterCrossAxisAlignment : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return size / 2
}
}
private object StartCrossAxisAlignment : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return if (layoutDirection == LayoutDirection.Ltr) 0 else size
}
}
private object EndCrossAxisAlignment : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return if (layoutDirection == LayoutDirection.Ltr) size else 0
}
}
private class AlignmentLineCrossAxisAlignment(
val alignmentLineProvider: AlignmentLineProvider
) : CrossAxisAlignment() {
override val isRelative: Boolean
get() = true
override fun calculateAlignmentLinePosition(placeable: Placeable): Int {
return alignmentLineProvider.calculateAlignmentLinePosition(placeable)
}
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
val alignmentLinePosition =
alignmentLineProvider.calculateAlignmentLinePosition(placeable)
return if (alignmentLinePosition != AlignmentLine.Unspecified) {
val line = beforeCrossAxisAlignmentLine - alignmentLinePosition
if (layoutDirection == LayoutDirection.Rtl) {
size - line
} else {
line
}
} else {
0
}
}
}
private class VerticalCrossAxisAlignment(
val vertical: Alignment.Vertical
) : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return vertical.align(0, size)
}
}
private class HorizontalCrossAxisAlignment(
val horizontal: Alignment.Horizontal
) : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return horizontal.align(0, size, layoutDirection)
}
}
}
/**
* Box [Constraints], but which abstract away width and height in favor of main axis and cross axis.
*/
internal data class OrientationIndependentConstraints(
val mainAxisMin: Int,
val mainAxisMax: Int,
val crossAxisMin: Int,
val crossAxisMax: Int
) {
constructor(c: Constraints, orientation: LayoutOrientation) : this(
if (orientation === Horizontal) c.minWidth else c.minHeight,
if (orientation === Horizontal) c.maxWidth else c.maxHeight,
if (orientation === Horizontal) c.minHeight else c.minWidth,
if (orientation === Horizontal) c.maxHeight else c.maxWidth
)
// Creates a new instance with the same main axis constraints and maximum tight cross axis.
fun stretchCrossAxis() = OrientationIndependentConstraints(
mainAxisMin,
mainAxisMax,
if (crossAxisMax != Constraints.Infinity) crossAxisMax else crossAxisMin,
crossAxisMax
)
// Given an orientation, resolves the current instance to traditional constraints.
fun toBoxConstraints(orientation: LayoutOrientation) =
if (orientation === Horizontal) {
Constraints(mainAxisMin, mainAxisMax, crossAxisMin, crossAxisMax)
} else {
Constraints(crossAxisMin, crossAxisMax, mainAxisMin, mainAxisMax)
}
// Given an orientation, resolves the max width constraint this instance represents.
fun maxWidth(orientation: LayoutOrientation) =
if (orientation === Horizontal) {
mainAxisMax
} else {
crossAxisMax
}
// Given an orientation, resolves the max height constraint this instance represents.
fun maxHeight(orientation: LayoutOrientation) =
if (orientation === Horizontal) {
crossAxisMax
} else {
mainAxisMax
}
}
internal val IntrinsicMeasurable.rowColumnParentData: RowColumnParentData?
get() = parentData as? RowColumnParentData
internal val RowColumnParentData?.weight: Float
get() = this?.weight ?: 0f
internal val RowColumnParentData?.fill: Boolean
get() = this?.fill ?: true
internal val RowColumnParentData?.crossAxisAlignment: CrossAxisAlignment?
get() = this?.crossAxisAlignment
internal val RowColumnParentData?.isRelative: Boolean
get() = this.crossAxisAlignment?.isRelative ?: false
internal fun MinIntrinsicWidthMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMinWidth
} else {
IntrinsicMeasureBlocks.VerticalMinWidth
}
internal fun MinIntrinsicHeightMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMinHeight
} else {
IntrinsicMeasureBlocks.VerticalMinHeight
}
internal fun MaxIntrinsicWidthMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMaxWidth
} else {
IntrinsicMeasureBlocks.VerticalMaxWidth
}
internal fun MaxIntrinsicHeightMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMaxHeight
} else {
IntrinsicMeasureBlocks.VerticalMaxHeight
}
internal object IntrinsicMeasureBlocks {
val HorizontalMinWidth: (List, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Horizontal,
Horizontal
)
}
val VerticalMinWidth: (List, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Vertical,
Horizontal
)
}
val HorizontalMinHeight: (List, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Horizontal,
Vertical
)
}
val VerticalMinHeight: (List, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Vertical,
Vertical
)
}
val HorizontalMaxWidth: (List, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Horizontal,
Horizontal
)
}
val VerticalMaxWidth: (List, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Vertical,
Horizontal
)
}
val HorizontalMaxHeight: (List, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Horizontal,
Vertical
)
}
val VerticalMaxHeight: (List, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Vertical,
Vertical
)
}
}
private fun intrinsicSize(
children: List,
intrinsicMainSize: IntrinsicMeasurable.(Int) -> Int,
intrinsicCrossSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int,
layoutOrientation: LayoutOrientation,
intrinsicOrientation: LayoutOrientation
) = if (layoutOrientation == intrinsicOrientation) {
intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable, mainAxisSpacing)
} else {
intrinsicCrossAxisSize(
children,
intrinsicCrossSize,
intrinsicMainSize,
crossAxisAvailable,
mainAxisSpacing
)
}
private fun intrinsicMainAxisSize(
children: List,
mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int
): Int {
var weightUnitSpace = 0
var fixedSpace = 0
var totalWeight = 0f
children.fastForEach { child ->
val weight = child.rowColumnParentData.weight
val size = child.mainAxisSize(crossAxisAvailable)
if (weight == 0f) {
fixedSpace += size
} else if (weight > 0f) {
totalWeight += weight
weightUnitSpace = max(weightUnitSpace, (size / weight).roundToInt())
}
}
return (weightUnitSpace * totalWeight).roundToInt() + fixedSpace +
(children.size - 1) * mainAxisSpacing
}
private fun intrinsicCrossAxisSize(
children: List,
mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisSize: IntrinsicMeasurable.(Int) -> Int,
mainAxisAvailable: Int,
mainAxisSpacing: Int
): Int {
var fixedSpace = min((children.size - 1) * mainAxisSpacing, mainAxisAvailable)
var crossAxisMax = 0
var totalWeight = 0f
children.fastForEach { child ->
val weight = child.rowColumnParentData.weight
if (weight == 0f) {
// Ask the child how much main axis space it wants to occupy. This cannot be more
// than the remaining available space.
val mainAxisSpace = min(
child.mainAxisSize(Constraints.Infinity),
mainAxisAvailable - fixedSpace
)
fixedSpace += mainAxisSpace
// Now that the assigned main axis space is known, ask about the cross axis space.
crossAxisMax = max(crossAxisMax, child.crossAxisSize(mainAxisSpace))
} else if (weight > 0f) {
totalWeight += weight
}
}
// For weighted children, calculate how much main axis space weight=1 would represent.
val weightUnitSpace = if (totalWeight == 0f) {
0
} else if (mainAxisAvailable == Constraints.Infinity) {
Constraints.Infinity
} else {
(max(mainAxisAvailable - fixedSpace, 0) / totalWeight).roundToInt()
}
children.fastForEach { child ->
val weight = child.rowColumnParentData.weight
// Now the main axis for weighted children is known, so ask about the cross axis space.
if (weight > 0f) {
crossAxisMax = max(
crossAxisMax,
child.crossAxisSize(
if (weightUnitSpace != Constraints.Infinity) {
(weightUnitSpace * weight).roundToInt()
} else {
Constraints.Infinity
}
)
)
}
}
return crossAxisMax
}
internal class LayoutWeightImpl(
val weight: Float,
val fill: Boolean,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?) =
((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.weight = weight
it.fill = fill
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? LayoutWeightImpl ?: return false
return weight == otherModifier.weight &&
fill == otherModifier.fill
}
override fun hashCode(): Int {
var result = weight.hashCode()
result = 31 * result + fill.hashCode()
return result
}
override fun toString(): String =
"LayoutWeightImpl(weight=$weight, fill=$fill)"
}
internal sealed class SiblingsAlignedModifier(
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
abstract override fun Density.modifyParentData(parentData: Any?): Any?
internal class WithAlignmentLineBlock(
val block: (Measured) -> Int,
inspectorInfo: InspectorInfo.() -> Unit
) : SiblingsAlignedModifier(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): Any {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment =
CrossAxisAlignment.Relative(AlignmentLineProvider.Block(block))
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? WithAlignmentLineBlock ?: return false
return block == otherModifier.block
}
override fun hashCode(): Int = block.hashCode()
override fun toString(): String = "WithAlignmentLineBlock(block=$block)"
}
internal class WithAlignmentLine(
val alignmentLine: AlignmentLine,
inspectorInfo: InspectorInfo.() -> Unit
) : SiblingsAlignedModifier(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): Any {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment =
CrossAxisAlignment.Relative(AlignmentLineProvider.Value(alignmentLine))
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? WithAlignmentLine ?: return false
return alignmentLine == otherModifier.alignmentLine
}
override fun hashCode(): Int = alignmentLine.hashCode()
override fun toString(): String = "WithAlignmentLine(line=$alignmentLine)"
}
}
internal class HorizontalAlignModifier(
val horizontal: Alignment.Horizontal,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): RowColumnParentData {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment = CrossAxisAlignment.horizontal(horizontal)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? HorizontalAlignModifier ?: return false
return horizontal == otherModifier.horizontal
}
override fun hashCode(): Int = horizontal.hashCode()
override fun toString(): String =
"HorizontalAlignModifier(horizontal=$horizontal)"
}
internal class VerticalAlignModifier(
val vertical: Alignment.Vertical,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): RowColumnParentData {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment = CrossAxisAlignment.vertical(vertical)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? VerticalAlignModifier ?: return false
return vertical == otherModifier.vertical
}
override fun hashCode(): Int = vertical.hashCode()
override fun toString(): String =
"VerticalAlignModifier(vertical=$vertical)"
}
/**
* Provides the alignment line.
*/
internal sealed class AlignmentLineProvider {
abstract fun calculateAlignmentLinePosition(placeable: Placeable): Int
data class Block(val lineProviderBlock: (Measured) -> Int) : AlignmentLineProvider() {
override fun calculateAlignmentLinePosition(
placeable: Placeable
): Int {
return lineProviderBlock(placeable)
}
}
data class Value(val alignmentLine: AlignmentLine) : AlignmentLineProvider() {
override fun calculateAlignmentLinePosition(placeable: Placeable): Int {
return placeable[alignmentLine]
}
}
}
/**
* Used to specify how a layout chooses its own size when multiple behaviors are possible.
*/
// TODO(popam): remove this when Flow is reworked
internal enum class SizeMode {
/**
* Minimize the amount of free space by wrapping the children,
* subject to the incoming layout constraints.
*/
Wrap,
/**
* Maximize the amount of free space by expanding to fill the available space,
* subject to the incoming layout constraints.
*/
Expand
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/RowColumnMeasurementHelper.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
/**
* Copied from:
* RowColumnMeasurementHelper.kt
* https://android-review.googlesource.com/c/platform/frameworks/support/+/2260390/27/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurementHelper.kt
*
* The only changes were updating access modifiers and making RowColumnMeasurementHelper an open class
*/
/**
* This is a data class that holds the determined width, height of a row,
* and information on how to retrieve main axis and cross axis positions.
*/
internal class RowColumnMeasureHelperResult(
val crossAxisSize: Int,
val mainAxisSize: Int,
val startIndex: Int,
val endIndex: Int,
val beforeCrossAxisAlignmentLine: Int,
val mainAxisPositions: IntArray,
)
/**
* RowColumnMeasurementHelper
* Measures the row and column without placing, useful for reusing row/column logic
*/
internal open class RowColumnMeasurementHelper(
val orientation: LayoutOrientation,
val arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
val arrangementSpacing: Dp,
val crossAxisSize: SizeMode,
val crossAxisAlignment: CrossAxisAlignment,
val measurables: List,
val placeables: Array
) {
private val rowColumnParentData = Array(measurables.size) {
measurables[it].rowColumnParentData
}
fun Placeable.mainAxisSize() =
if (orientation == LayoutOrientation.Horizontal) width else height
fun Placeable.crossAxisSize() =
if (orientation == LayoutOrientation.Horizontal) height else width
/**
* Measures the row and column without placing, useful for reusing row/column logic
*
* @param measureScope The measure scope to retrieve density
* @param constraints The desired constraints for the startIndex and endIndex
* can hold null items if not measured.
* @param startIndex The startIndex (inclusive) when examining measurables, placeable
* and parentData
* @param endIndex The ending index (exclusive) when examinning measurable, placeable
* and parentData
*/
fun measureWithoutPlacing(
measureScope: MeasureScope,
constraints: Constraints,
startIndex: Int,
endIndex: Int
): RowColumnMeasureHelperResult {
@Suppress("NAME_SHADOWING")
val constraints = OrientationIndependentConstraints(constraints, orientation)
val arrangementSpacingPx = with(measureScope) {
arrangementSpacing.roundToPx()
}
var totalWeight = 0f
var fixedSpace = 0
var crossAxisSpace = 0
var weightChildrenCount = 0
var anyAlignBy = false
val subSize = endIndex - startIndex
// First measure children with zero weight.
var spaceAfterLastNoWeight = 0
for (i in startIndex until endIndex) {
val child = measurables[i]
val parentData = rowColumnParentData[i]
val weight = parentData.weight
if (weight > 0f) {
totalWeight += weight
++weightChildrenCount
} else {
val mainAxisMax = constraints.mainAxisMax
val placeable = placeables[i] ?: child.measure(
// Ask for preferred main axis size.
constraints.copy(
mainAxisMin = 0,
mainAxisMax = if (mainAxisMax == Constraints.Infinity) {
Constraints.Infinity
} else {
mainAxisMax - fixedSpace
},
crossAxisMin = 0
).toBoxConstraints(orientation)
)
spaceAfterLastNoWeight = min(
arrangementSpacingPx,
mainAxisMax - fixedSpace - placeable.mainAxisSize()
)
fixedSpace += placeable.mainAxisSize() + spaceAfterLastNoWeight
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
anyAlignBy = anyAlignBy || parentData.isRelative
placeables[i] = placeable
}
}
var weightedSpace = 0
if (weightChildrenCount == 0) {
// fixedSpace contains an extra spacing after the last non-weight child.
fixedSpace -= spaceAfterLastNoWeight
} else {
// Measure the rest according to their weights in the remaining main axis space.
val targetSpace =
if (totalWeight > 0f && constraints.mainAxisMax != Constraints.Infinity) {
constraints.mainAxisMax
} else {
constraints.mainAxisMin
}
val remainingToTarget =
targetSpace - fixedSpace - arrangementSpacingPx * (weightChildrenCount - 1)
val weightUnitSpace = if (totalWeight > 0) remainingToTarget / totalWeight else 0f
var remainder = remainingToTarget - (startIndex until endIndex).sumOf {
(weightUnitSpace * rowColumnParentData[it].weight).roundToInt()
}
for (i in startIndex until endIndex) {
if (placeables[i] == null) {
val child = measurables[i]
val parentData = rowColumnParentData[i]
val weight = parentData.weight
check(weight > 0) { "All weights <= 0 should have placeables" }
// After the weightUnitSpace rounding, the total space going to be occupied
// can be smaller or larger than remainingToTarget. Here we distribute the
// loss or gain remainder evenly to the first children.
val remainderUnit = remainder.sign
remainder -= remainderUnit
val childMainAxisSize = max(
0,
(weightUnitSpace * weight).roundToInt() + remainderUnit
)
val placeable = child.measure(
OrientationIndependentConstraints(
if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
childMainAxisSize
} else {
0
},
childMainAxisSize,
0,
constraints.crossAxisMax
).toBoxConstraints(orientation)
)
weightedSpace += placeable.mainAxisSize()
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
anyAlignBy = anyAlignBy || parentData.isRelative
placeables[i] = placeable
}
}
weightedSpace = (weightedSpace + arrangementSpacingPx * (weightChildrenCount - 1))
.coerceAtMost(constraints.mainAxisMax - fixedSpace)
}
var beforeCrossAxisAlignmentLine = 0
var afterCrossAxisAlignmentLine = 0
if (anyAlignBy) {
for (i in startIndex until endIndex) {
val placeable = placeables[i]!!
val parentData = rowColumnParentData[i]
val alignmentLinePosition = parentData.crossAxisAlignment
?.calculateAlignmentLinePosition(placeable)
if (alignmentLinePosition != null) {
beforeCrossAxisAlignmentLine = max(
beforeCrossAxisAlignmentLine,
alignmentLinePosition.let {
if (it != AlignmentLine.Unspecified) it else 0
}
)
afterCrossAxisAlignmentLine = max(
afterCrossAxisAlignmentLine,
placeable.crossAxisSize() -
(
alignmentLinePosition.let {
if (it != AlignmentLine.Unspecified) {
it
} else {
placeable.crossAxisSize()
}
}
)
)
}
}
}
// Compute the Row or Column size and position the children.
val mainAxisLayoutSize = max(fixedSpace + weightedSpace, constraints.mainAxisMin)
val crossAxisLayoutSize = if (constraints.crossAxisMax != Constraints.Infinity &&
crossAxisSize == SizeMode.Expand
) {
constraints.crossAxisMax
} else {
max(
crossAxisSpace,
max(
constraints.crossAxisMin,
beforeCrossAxisAlignmentLine + afterCrossAxisAlignmentLine
)
)
}
val mainAxisPositions = IntArray(subSize) { 0 }
val childrenMainAxisSize = IntArray(subSize) { index ->
placeables[index + startIndex]!!.mainAxisSize()
}
return RowColumnMeasureHelperResult(
mainAxisSize = mainAxisLayoutSize,
crossAxisSize = crossAxisLayoutSize,
startIndex = startIndex,
endIndex = endIndex,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine,
mainAxisPositions = mainAxisPositions(
mainAxisLayoutSize,
childrenMainAxisSize,
mainAxisPositions,
measureScope
)
)
}
private fun mainAxisPositions(
mainAxisLayoutSize: Int,
childrenMainAxisSize: IntArray,
mainAxisPositions: IntArray,
measureScope: MeasureScope
): IntArray {
arrangement(
mainAxisLayoutSize,
childrenMainAxisSize,
measureScope.layoutDirection,
measureScope,
mainAxisPositions
)
return mainAxisPositions
}
protected fun getCrossAxisPosition(
placeable: Placeable,
parentData: RowColumnParentData?,
crossAxisLayoutSize: Int,
layoutDirection: LayoutDirection,
beforeCrossAxisAlignmentLine: Int
): Int {
val childCrossAlignment = parentData?.crossAxisAlignment ?: crossAxisAlignment
return childCrossAlignment.align(
size = crossAxisLayoutSize - placeable.crossAxisSize(),
layoutDirection = if (orientation == LayoutOrientation.Horizontal) {
LayoutDirection.Ltr
} else {
layoutDirection
},
placeable = placeable,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine
)
}
fun placeHelper(
placeableScope: Placeable.PlacementScope,
measureResult: RowColumnMeasureHelperResult,
crossAxisOffset: Int,
layoutDirection: LayoutDirection,
) {
with(placeableScope) {
for (i in measureResult.startIndex until measureResult.endIndex) {
val placeable = placeables[i]
placeable!!
val mainAxisPositions = measureResult.mainAxisPositions
val crossAxisPosition = getCrossAxisPosition(
placeable,
(measurables[i].parentData as? RowColumnParentData),
measureResult.crossAxisSize,
layoutDirection,
measureResult.beforeCrossAxisAlignmentLine
) + crossAxisOffset
if (orientation == LayoutOrientation.Horizontal) {
placeable.place(
mainAxisPositions[i - measureResult.startIndex],
crossAxisPosition
)
} else {
placeable.place(
crossAxisPosition,
mainAxisPositions[i - measureResult.startIndex]
)
}
}
}
}
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import kotlin.math.roundToInt
/**
* A layout that places two different pieces of content defined by the [first] and [second]
* slots where the arrangement, sizes and separation behaviour is controlled by [TwoPaneStrategy].
*
* [TwoPane] is fold and hinges aware using the provided the [displayFeatures] (which should
* normally be calculated via [calculateDisplayFeatures]). The layout will be adapted to properly
* separate [first] and [second] panes so they don't interfere with hardware hinges (vertical or
* horizontal) as specified in [displayFeatures], or respect folds when needed (for example, when
* foldable is half-folded (90-degree fold AKA tabletop) the split will become on the bend).
*
* To only be aware of folds with a specific orientation, pass in an alternate
* [foldAwareConfiguration] to only adjust for vertical or horizontal folds.
*
* The [TwoPane] layout will always place both [first] and [second], based on the provided
* [strategy] and window environment. If you instead only want to place one or the other,
* that should be controlled at a higher level and not calling [TwoPane] if placing both is not
* desired.
*
* @param first the first content of the layout, a left-most in LTR, a right-most in RTL and
* top-most in a vertical split based on the [SplitResult] of [TwoPaneStrategy.calculateSplitResult]
* @param second the second content of the layout, a right-most in the LTR, a left-most in the RTL
* and the bottom-most in a vertical split based on the [SplitResult] of
* [TwoPaneStrategy.calculateSplitResult]
* @param strategy strategy of the two pane that controls the arrangement of the layout
* @param displayFeatures the list of known display features to automatically avoid
* @param foldAwareConfiguration the types of display features to automatically avoid
* @param modifier an optional modifier for the layout
*/
@Composable
public fun TwoPane(
first: @Composable () -> Unit,
second: @Composable () -> Unit,
strategy: TwoPaneStrategy,
displayFeatures: List,
modifier: Modifier = Modifier,
foldAwareConfiguration: FoldAwareConfiguration = FoldAwareConfiguration.AllFolds,
) {
TwoPane(
first = first,
second = second,
strategy = when (foldAwareConfiguration) {
FoldAwareConfiguration.HorizontalFoldsOnly -> {
VerticalTwoPaneStrategy(
displayFeatures = displayFeatures,
defaultStrategy = strategy,
)
}
FoldAwareConfiguration.VerticalFoldsOnly -> {
HorizontalTwoPaneStrategy(
displayFeatures = displayFeatures,
defaultStrategy = strategy,
)
}
FoldAwareConfiguration.AllFolds -> {
TwoPaneStrategy(
displayFeatures = displayFeatures,
defaultStrategy = strategy,
)
}
else -> error("Unknown FoldAware value!")
},
modifier = modifier,
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal fun TwoPane(
first: @Composable () -> Unit,
second: @Composable () -> Unit,
strategy: TwoPaneStrategy,
modifier: Modifier = Modifier
) {
val density = LocalDensity.current
Layout(
modifier = modifier.wrapContentSize(),
content = {
Box(Modifier.layoutId("first")) {
first()
}
Box(Modifier.layoutId("second")) {
second()
}
}
) { measurable, constraints ->
val firstMeasurable = measurable.find { it.layoutId == "first" }!!
val secondMeasurable = measurable.find { it.layoutId == "second" }!!
layout(constraints.maxWidth, constraints.maxHeight) {
val splitResult = strategy.calculateSplitResult(
density = density,
layoutDirection = layoutDirection,
layoutCoordinates = coordinates ?: return@layout
)
val gapOrientation = splitResult.gapOrientation
val gapBounds = splitResult.gapBounds
val gapLeft = constraints.constrainWidth(gapBounds.left.roundToInt())
val gapRight = constraints.constrainWidth(gapBounds.right.roundToInt())
val gapTop = constraints.constrainHeight(gapBounds.top.roundToInt())
val gapBottom = constraints.constrainHeight(gapBounds.bottom.roundToInt())
val firstConstraints =
if (gapOrientation == Orientation.Vertical) {
val width = when (layoutDirection) {
LayoutDirection.Ltr -> gapLeft
LayoutDirection.Rtl -> constraints.maxWidth - gapRight
}
constraints.copy(minWidth = width, maxWidth = width)
} else {
constraints.copy(minHeight = gapTop, maxHeight = gapTop)
}
val secondConstraints =
if (gapOrientation == Orientation.Vertical) {
val width = when (layoutDirection) {
LayoutDirection.Ltr -> constraints.maxWidth - gapRight
LayoutDirection.Rtl -> gapLeft
}
constraints.copy(minWidth = width, maxWidth = width)
} else {
val height = constraints.maxHeight - gapBottom
constraints.copy(minHeight = height, maxHeight = height)
}
val firstPlaceable = firstMeasurable.measure(constraints.constrain(firstConstraints))
val secondPlaceable = secondMeasurable.measure(constraints.constrain(secondConstraints))
firstPlaceable.placeRelative(0, 0)
val detailOffsetX =
if (gapOrientation == Orientation.Vertical) {
constraints.maxWidth - secondPlaceable.width
} else {
0
}
val detailOffsetY =
if (gapOrientation == Orientation.Vertical) {
0
} else {
constraints.maxHeight - secondPlaceable.height
}
secondPlaceable.placeRelative(detailOffsetX, detailOffsetY)
}
}
}
/**
* The configuration for which type of folds for a [TwoPane] to automatically avoid.
*/
@JvmInline
public value class FoldAwareConfiguration private constructor(private val value: Int) {
public companion object {
/**
* The [TwoPane] will only be aware of horizontal folds only, splitting the content
* vertically.
*/
public val HorizontalFoldsOnly: FoldAwareConfiguration = FoldAwareConfiguration(0)
/**
* The [TwoPane] will only be aware of vertical folds only, splitting the content
* horizontally.
*/
public val VerticalFoldsOnly: FoldAwareConfiguration = FoldAwareConfiguration(1)
/**
* The [TwoPane] will be aware of both horizontal and vertical folds, splitting the content
* vertically and horizontally respectively.
*/
public val AllFolds: FoldAwareConfiguration = FoldAwareConfiguration(2)
}
}
/**
* Returns the specification for where to place a split in [TwoPane] as a result of
* [TwoPaneStrategy.calculateSplitResult]
*/
public class SplitResult(
/**
* Whether the gap is vertical or horizontal
*/
public val gapOrientation: Orientation,
/**
* The bounds that are nether a `start` pane or an `end` pane, but a separation between those
* two. In case width or height is 0 - it means that the gap itself is a 0 width/height, but the
* place within the layout is still defined.
*
* The [gapBounds] should be defined in local bounds to the [TwoPane].
*/
public val gapBounds: Rect,
)
/**
* A strategy for configuring the [TwoPane] component, that is responsible for the meta-data
* corresponding to the arrangement of the two panes of the layout.
*/
public fun interface TwoPaneStrategy {
/**
* Calculates the split result in local bounds of the [TwoPane].
*
* @param density the [Density] for measuring and laying out
* @param layoutDirection the [LayoutDirection] for measuring and laying out
* @param layoutCoordinates the [LayoutCoordinates] of the [TwoPane]
*/
public fun calculateSplitResult(
density: Density,
layoutDirection: LayoutDirection,
layoutCoordinates: LayoutCoordinates
): SplitResult
}
/**
* A strategy for configuring the [TwoPane] component, that is responsible for the meta-data
* corresponding to the arrangement of the two panes of the layout.
*
* This strategy can be conditional: If `null` is returned from [calculateSplitResult], then this
* strategy did not produce a split result to use, and a different strategy should be used.
*/
private fun interface ConditionalTwoPaneStrategy {
/**
* Calculates the split result in local bounds of the [TwoPane], or `null` if this strategy
* does not apply.
*
* @param density the [Density] for measuring and laying out
* @param layoutDirection the [LayoutDirection] for measuring and laying out
* @param layoutCoordinates the [LayoutCoordinates] of the [TwoPane]
*/
fun calculateSplitResult(
density: Density,
layoutDirection: LayoutDirection,
layoutCoordinates: LayoutCoordinates
): SplitResult?
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at the given [splitFraction] from start, with the given
* [gapWidth].
*/
public fun HorizontalTwoPaneStrategy(
splitFraction: Float,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = FractionHorizontalTwoPaneStrategy(
splitFraction = splitFraction,
gapWidth = gapWidth
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at [splitOffset] either from the start or end based on
* [offsetFromStart], with the given [gapWidth].
*/
public fun HorizontalTwoPaneStrategy(
splitOffset: Dp,
offsetFromStart: Boolean = true,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = splitOffset,
offsetFromStart = offsetFromStart,
gapWidth = gapWidth
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at the given [splitFraction] from top, with the given
* [gapHeight].
*/
public fun VerticalTwoPaneStrategy(
splitFraction: Float,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = FractionVerticalTwoPaneStrategy(
splitFraction = splitFraction,
gapHeight = gapHeight
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at [splitOffset] either from the top or bottom based on
* [offsetFromTop], with the given [gapHeight].
*/
public fun VerticalTwoPaneStrategy(
splitOffset: Dp,
offsetFromTop: Boolean = true,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = splitOffset,
offsetFromTop = offsetFromTop,
gapHeight = gapHeight
)
/**
* Returns a [TwoPaneStrategy] that will place the slots vertically or horizontally if there is a
* horizontal or vertical fold respectively.
*
* If there is no fold, then the [defaultStrategy] will be used instead.
*/
private fun TwoPaneStrategy(
displayFeatures: List,
defaultStrategy: TwoPaneStrategy,
): TwoPaneStrategy = TwoPaneStrategy(
FoldAwareHorizontalTwoPaneStrategy(displayFeatures),
FoldAwareVerticalTwoPaneStrategy(displayFeatures),
defaultStrategy = defaultStrategy
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally if there is a vertical fold.
*
* If there is no fold, then the [defaultStrategy] will be used instead.
*/
private fun HorizontalTwoPaneStrategy(
displayFeatures: List,
defaultStrategy: TwoPaneStrategy,
): TwoPaneStrategy = TwoPaneStrategy(
FoldAwareHorizontalTwoPaneStrategy(displayFeatures),
defaultStrategy = defaultStrategy
)
/**
* Returns a [TwoPaneStrategy] that will place the slots vertically if there is a horizontal fold.
*
* If there is no fold, then the [defaultStrategy] will be used instead.
*/
private fun VerticalTwoPaneStrategy(
displayFeatures: List,
defaultStrategy: TwoPaneStrategy,
): TwoPaneStrategy = TwoPaneStrategy(
FoldAwareVerticalTwoPaneStrategy(displayFeatures),
defaultStrategy = defaultStrategy
)
/**
* Returns a composite [TwoPaneStrategy].
*
* The conditional strategies (if any) will be attempted in order, and their split result used
* if they return one. If none return a split result, then the [defaultStrategy] will be used,
* which guarantees returning a [SplitResult].
*/
private fun TwoPaneStrategy(
vararg conditionalStrategies: ConditionalTwoPaneStrategy,
defaultStrategy: TwoPaneStrategy
): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates ->
conditionalStrategies.firstNotNullOfOrNull { conditionalTwoPaneStrategy ->
conditionalTwoPaneStrategy.calculateSplitResult(
density = density,
layoutDirection = layoutDirection,
layoutCoordinates = layoutCoordinates
)
} ?: defaultStrategy.calculateSplitResult(
density = density,
layoutDirection = layoutDirection,
layoutCoordinates = layoutCoordinates
)
}
/**
* Returns a [ConditionalTwoPaneStrategy] that will place the slots horizontally if there is a
* vertical fold, or `null` if there is no fold.
*/
private fun FoldAwareHorizontalTwoPaneStrategy(
displayFeatures: List,
): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates ->
val verticalFold = displayFeatures.find {
it is FoldingFeature && it.orientation == FoldingFeature.Orientation.VERTICAL
} as FoldingFeature?
if (verticalFold != null &&
(
verticalFold.isSeparating ||
verticalFold.occlusionType == FoldingFeature.OcclusionType.FULL
) &&
verticalFold.bounds.toComposeRect().overlaps(layoutCoordinates.boundsInWindow())
) {
val foldBounds = verticalFold.bounds.toComposeRect()
SplitResult(
gapOrientation = Orientation.Vertical,
gapBounds = Rect(
layoutCoordinates.windowToLocal(foldBounds.topLeft),
layoutCoordinates.windowToLocal(foldBounds.bottomRight)
)
)
} else {
null
}
}
/**
* Returns a [ConditionalTwoPaneStrategy] that will place the slots vertically if there is a
* horizontal fold, or `null` if there is no fold.
*/
private fun FoldAwareVerticalTwoPaneStrategy(
displayFeatures: List,
): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates ->
val horizontalFold = displayFeatures.find {
it is FoldingFeature && it.orientation == FoldingFeature.Orientation.HORIZONTAL
} as FoldingFeature?
if (horizontalFold != null &&
(
horizontalFold.isSeparating ||
horizontalFold.occlusionType == FoldingFeature.OcclusionType.FULL
) &&
horizontalFold.bounds.toComposeRect().overlaps(layoutCoordinates.boundsInWindow())
) {
val foldBounds = horizontalFold.bounds.toComposeRect()
SplitResult(
gapOrientation = Orientation.Horizontal,
gapBounds = Rect(
layoutCoordinates.windowToLocal(foldBounds.topLeft),
layoutCoordinates.windowToLocal(foldBounds.bottomRight)
)
)
} else {
null
}
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at the given [splitFraction] from start, with the given [gapWidth].
*
* This strategy is _not_ fold aware.
*/
internal fun FractionHorizontalTwoPaneStrategy(
splitFraction: Float,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates ->
val splitX = layoutCoordinates.size.width * when (layoutDirection) {
LayoutDirection.Ltr -> splitFraction
LayoutDirection.Rtl -> 1 - splitFraction
}
val splitWidthPixel = with(density) { gapWidth.toPx() }
SplitResult(
gapOrientation = Orientation.Vertical,
gapBounds = Rect(
left = splitX - splitWidthPixel / 2f,
top = 0f,
right = splitX + splitWidthPixel / 2f,
bottom = layoutCoordinates.size.height.toFloat(),
)
)
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at [splitOffset] either from the start or end based on
* [offsetFromStart], with the given [gapWidth].
*
* This strategy is _not_ fold aware.
*/
internal fun FixedOffsetHorizontalTwoPaneStrategy(
splitOffset: Dp,
offsetFromStart: Boolean,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates ->
val splitOffsetPixel = with(density) { splitOffset.toPx() }
val splitX = when (layoutDirection) {
LayoutDirection.Ltr ->
if (offsetFromStart) {
splitOffsetPixel
} else {
layoutCoordinates.size.width - splitOffsetPixel
}
LayoutDirection.Rtl ->
if (offsetFromStart) {
layoutCoordinates.size.width - splitOffsetPixel
} else {
splitOffsetPixel
}
}
val splitWidthPixel = with(density) { gapWidth.toPx() }
SplitResult(
gapOrientation = Orientation.Vertical,
gapBounds = Rect(
left = splitX - splitWidthPixel / 2f,
top = 0f,
right = splitX + splitWidthPixel / 2f,
bottom = layoutCoordinates.size.height.toFloat(),
)
)
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The split will be placed at the given [splitFraction] from start, with the given [gapHeight].
*
* This strategy is _not_ fold aware.
*/
internal fun FractionVerticalTwoPaneStrategy(
splitFraction: Float,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates ->
val splitY = layoutCoordinates.size.height * splitFraction
val splitHeightPixel = with(density) { gapHeight.toPx() }
SplitResult(
gapOrientation = Orientation.Horizontal,
gapBounds = Rect(
left = 0f,
top = splitY - splitHeightPixel / 2f,
right = layoutCoordinates.size.width.toFloat(),
bottom = splitY + splitHeightPixel / 2f,
)
)
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The split will be placed at [splitOffset] either from the top or bottom based on
* [offsetFromTop], with the given [gapHeight].
*
* This strategy is _not_ fold aware.
*/
internal fun FixedOffsetVerticalTwoPaneStrategy(
splitOffset: Dp,
offsetFromTop: Boolean,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates ->
val splitOffsetPixel = with(density) { splitOffset.toPx() }
val splitY =
if (offsetFromTop) {
splitOffsetPixel
} else {
layoutCoordinates.size.height - splitOffsetPixel
}
val splitHeightPixel = with(density) { gapHeight.toPx() }
SplitResult(
gapOrientation = Orientation.Horizontal,
gapBounds = Rect(
left = 0f,
top = splitY - splitHeightPixel / 2f,
right = layoutCoordinates.size.width.toFloat(),
bottom = splitY + splitHeightPixel / 2f,
)
)
}
================================================
FILE: adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/DisplayFeaturesTest.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowLayoutInfo
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DisplayFeaturesTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
@get:Rule
val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule()
@Test
fun empty_folding_features_is_correct() {
lateinit var displayFeatures: List
composeTestRule.setContent {
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity)
}
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList()))
composeTestRule.waitForIdle()
assertThat(displayFeatures).isEmpty()
}
@Test
fun single_folding_features_is_correct() {
lateinit var displayFeatures: List
composeTestRule.setContent {
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity)
}
val fakeFoldingFeature = FoldingFeature(
activity = composeTestRule.activity,
center = 200,
size = 40,
state = FoldingFeature.State.HALF_OPENED,
orientation = FoldingFeature.Orientation.VERTICAL,
)
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(
WindowLayoutInfo(
listOf(
fakeFoldingFeature
)
)
)
composeTestRule.waitForIdle()
assertThat(displayFeatures).hasSize(1)
assertThat(displayFeatures[0]).isEqualTo(fakeFoldingFeature)
}
@Test
fun updating_folding_features_is_correct() {
lateinit var displayFeatures: List
composeTestRule.setContent {
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity)
}
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList()))
val fakeFoldingFeature = FoldingFeature(
activity = composeTestRule.activity,
center = 200,
size = 40,
state = FoldingFeature.State.HALF_OPENED,
orientation = FoldingFeature.Orientation.VERTICAL,
)
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(
WindowLayoutInfo(
listOf(
fakeFoldingFeature
)
)
)
composeTestRule.waitForIdle()
assertThat(displayFeatures).hasSize(1)
assertThat(displayFeatures[0]).isEqualTo(fakeFoldingFeature)
}
}
================================================
FILE: adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/FoldAwareColumnTest.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import android.annotation.SuppressLint
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.google.accompanist.adaptive.FoldAwareColumnScopeInstance.ignoreFold
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class FoldAwareColumnTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
@get:Rule
val publisherRule = WindowLayoutInfoPublisherRule()
private val testTag = "FoldAwareColumnTestTag"
private var firstSpacerHeightDp = 0.dp
private var secondSpacerTopPx = 0f
private var secondSpacerBottomPx = 0f
@After
fun cleanUp() {
firstSpacerHeightDp = 0.dp
secondSpacerTopPx = 0f
secondSpacerBottomPx = 0f
}
@Test
fun second_spacer_placed_below_fold_with_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
val foldBoundsPx = simulateFoldingFeature()
assertEquals(foldBoundsPx.bottom, secondSpacerTopPx)
}
@Test
fun second_spacer_placed_below_fold_with_separating_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
val foldBoundsPx = simulateFoldingFeature(foldSizePx = 0)
assertEquals(foldBoundsPx.bottom, secondSpacerTopPx)
}
@Test
fun second_spacer_placed_below_first_spacer_without_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun second_spacer_placed_below_first_spacer_with_non_separating_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
simulateFoldingFeature(foldSizePx = 0, foldState = FoldingFeature.State.FLAT)
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun second_spacer_placed_below_first_spacer_with_vertical_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
simulateFoldingFeature(foldOrientation = FoldingFeature.Orientation.VERTICAL)
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun second_spacer_placed_below_first_spacer_with_ignore_fold_modifier() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers(secondSpacerModifier = Modifier.ignoreFold())
}
simulateFoldingFeature()
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun even_fold_padding_modifier_applies_around_hinge() {
val foldPaddingDp = 20.dp
lateinit var density: Density
composeTestRule.setContent {
density = LocalDensity.current
FoldAwareColumnWithSpacers(
foldPadding = PaddingValues(vertical = foldPaddingDp)
)
}
val foldBoundsPx = simulateFoldingFeature()
with(density) {
assertEquals(foldBoundsPx.bottom + foldPaddingDp.roundToPx(), secondSpacerTopPx)
}
}
@Test
fun uneven_fold_padding_modifier_applies_around_hinge() {
val foldPaddingBottom = 40.dp
lateinit var density: Density
composeTestRule.setContent {
density = LocalDensity.current
FoldAwareColumnWithSpacers(
foldPadding = PaddingValues(top = 15.dp, bottom = foldPaddingBottom)
)
}
val foldBoundsPx = simulateFoldingFeature()
with(density) {
assertEquals(foldBoundsPx.bottom + foldPaddingBottom.roundToPx(), secondSpacerTopPx)
}
}
@Test
fun layout_bounds_align_with_child_bounds_without_separating_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
val layoutBottomPx = composeTestRule.onRoot()
.fetchSemanticsNode().layoutInfo.coordinates.trueBoundsInWindow().bottom
assertEquals(layoutBottomPx, secondSpacerBottomPx)
}
@Test
fun layout_bounds_contain_child_bounds_when_placed_above_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers(
firstSpacerHeightPct = 0.1f,
secondSpacerHeightPct = 0.1f
)
}
simulateFoldingFeature()
val layoutBottomPx = composeTestRule.onRoot()
.fetchSemanticsNode().layoutInfo.coordinates.trueBoundsInWindow().bottom
assert(secondSpacerBottomPx <= layoutBottomPx)
}
@Test
fun layout_bounds_contain_child_bounds_when_placed_below_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
simulateFoldingFeature()
val layoutBottomPx = composeTestRule.onRoot()
.fetchSemanticsNode().layoutInfo.coordinates.trueBoundsInWindow().bottom
assert(secondSpacerBottomPx <= layoutBottomPx)
}
/**
* Test layout for FoldAwareColumn that includes two spacers with the provided heights
*/
@Composable
@SuppressLint("ModifierParameter")
private fun FoldAwareColumnWithSpacers(
foldPadding: PaddingValues = PaddingValues(),
firstSpacerHeightPct: Float = 0.25f,
secondSpacerHeightPct: Float = 0.25f,
secondSpacerModifier: Modifier = Modifier,
) {
var secondSpacerHeightDp: Dp
val metrics = remember(LocalConfiguration.current) {
WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(composeTestRule.activity)
}
with(LocalDensity.current) {
val windowHeight = metrics.bounds.height().toDp().value
firstSpacerHeightDp = (firstSpacerHeightPct * windowHeight).dp
secondSpacerHeightDp = (secondSpacerHeightPct * windowHeight).dp
}
FoldAwareColumn(
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity),
foldPadding = foldPadding,
) {
Spacer(
modifier = Modifier.height(firstSpacerHeightDp)
)
Spacer(
modifier = secondSpacerModifier
.height(secondSpacerHeightDp)
.testTag(testTag)
.onGloballyPositioned {
secondSpacerTopPx = it.positionInWindow().y
secondSpacerBottomPx = secondSpacerTopPx + it.size.height
}
)
}
}
/**
* Simulates a Jetpack Window Manager folding feature with the provided properties and returns
* the bounding box of the fold
*/
private fun simulateFoldingFeature(
foldSizePx: Int = 25,
foldState: FoldingFeature.State = FoldingFeature.State.HALF_OPENED,
foldOrientation: FoldingFeature.Orientation = FoldingFeature.Orientation.HORIZONTAL
): Rect {
val fakeFoldingFeature = FoldingFeature(
activity = composeTestRule.activity,
size = foldSizePx,
state = foldState,
orientation = foldOrientation,
)
publisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(listOf(fakeFoldingFeature)))
composeTestRule.waitForIdle()
return fakeFoldingFeature.bounds.toComposeRect()
}
}
================================================
FILE: adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.adaptive
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.DeviceConfigurationOverride
import androidx.compose.ui.test.ForcedSize
import androidx.compose.ui.test.LayoutDirection
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.then
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toIntRect
import androidx.compose.ui.unit.toOffset
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.core.ExperimentalWindowApi
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.testing.layout.FoldingFeature
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.roundToInt
@RunWith(AndroidJUnit4::class)
class TwoPaneTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun fraction_horizontal_renders_correctly_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(300.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(300.dp, 0.dp),
DpSize(600.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_horizontal_renders_correctly_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(600.dp, 0.dp),
DpSize(300.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(600.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_horizontal_renders_correctly_with_split_width_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(268.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(332.dp, 0.dp),
DpSize(568.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_horizontal_renders_correctly_with_split_width_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(632.dp, 0.dp),
DpSize(268.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(568.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_vertical_renders_correctly() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionVerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 400.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 400.dp),
DpSize(900.dp, 800.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_vertical_renders_correctly_with_split_height() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionVerticalTwoPaneStrategy(
splitFraction = 1f / 3f,
gapHeight = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 368.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 432.dp),
DpSize(900.dp, 768.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_horizontal_renders_correctly_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(200.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(200.dp, 0.dp),
DpSize(700.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_horizontal_renders_correctly_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(700.dp, 0.dp),
DpSize(200.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(700.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_renders_correctly_with_split_width_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(168.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(232.dp, 0.dp),
DpSize(668.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_renders_correctly_with_split_width_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(732.dp, 0.dp),
DpSize(168.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(668.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_top_renders_correctly() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = true
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 300.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 300.dp),
DpSize(900.dp, 900.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_top_renders_correctly_with_split_height() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = true,
gapHeight = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 268.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 332.dp),
DpSize(900.dp, 868.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_bottom_renders_correctly() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = false
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 900.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 900.dp),
DpSize(900.dp, 300.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_bottom_renders_correctly_with_split_height() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = false,
gapHeight = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 868.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 932.dp),
DpSize(900.dp, 268.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_fallback_when_no_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = emptyList()
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 400.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 400.dp),
DpSize(900.dp, 800.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_vertical_placing_when_occluding_horizontal_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 600.dp,
size = 0.dp,
state = FoldingFeature.State.HALF_OPENED,
orientation = FoldingFeature.Orientation.HORIZONTAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 600.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 600.dp),
DpSize(900.dp, 600.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_vertical_placing_when_separating_horizontal_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 600.dp,
size = 60.dp,
state = FoldingFeature.State.FLAT,
orientation = FoldingFeature.Orientation.HORIZONTAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 570.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 630.dp),
DpSize(900.dp, 570.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_fallback_when_non_occluding_horizontal_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 600.dp,
size = 0.dp,
state = FoldingFeature.State.FLAT,
orientation = FoldingFeature.Orientation.HORIZONTAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 400.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 400.dp),
DpSize(900.dp, 800.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_horizontal_placing_when_occluding_vertical_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 450.dp,
size = 0.dp,
state = FoldingFeature.State.HALF_OPENED,
orientation = FoldingFeature.Orientation.VERTICAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(450.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(450.dp, 0.dp),
DpSize(450.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_horizontal_placing_when_separating_vertical_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 450.dp,
size = 64.dp,
state = FoldingFeature.State.FLAT,
orientation = FoldingFeature.Orientation.VERTICAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(418.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(482.dp, 0.dp),
DpSize(418.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_fallback_when_non_occluding_vertical_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 450.dp,
size = 0.dp,
state = FoldingFeature.State.FLAT,
orientation = FoldingFeature.Orientation.VERTICAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 400.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 400.dp),
DpSize(900.dp, 800.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
}
private fun compareRectWithTolerance(
expected: Rect,
actual: Rect,
tolerance: Float,
) {
assertThat(actual.left).isWithin(tolerance).of(expected.left)
assertThat(actual.right).isWithin(tolerance).of(expected.right)
assertThat(actual.top).isWithin(tolerance).of(expected.top)
assertThat(actual.bottom).isWithin(tolerance).of(expected.bottom)
}
/**
* A descriptor of a [FoldingFeature] but with the [center] and [size] specified relative to the
* to the coordinates of the [TwoPane] layout.
*/
private data class LocalFoldingFeature(
val center: Dp,
val size: Dp,
val state: FoldingFeature.State,
val orientation: FoldingFeature.Orientation
)
/**
* A [List] that lazily constructs the backing delegate list by calling the provided lambda.
*/
private class DelegateList(
listFactory: () -> List
) : List {
val delegate by lazy(listFactory)
override val size: Int get() = delegate.size
override fun get(index: Int): T = delegate[index]
override fun isEmpty(): Boolean = delegate.isEmpty()
override fun iterator(): Iterator = delegate.iterator()
override fun listIterator(): ListIterator = delegate.listIterator()
override fun listIterator(index: Int): ListIterator = delegate.listIterator(index)
override fun subList(fromIndex: Int, toIndex: Int): List =
delegate.subList(fromIndex, toIndex)
override fun lastIndexOf(element: T): Int = delegate.lastIndexOf(element)
override fun indexOf(element: T): Int = delegate.indexOf(element)
override fun containsAll(elements: Collection): Boolean = delegate.containsAll(elements)
override fun contains(element: T): Boolean = delegate.contains(element)
}
/**
* Folding features are always expressed in window coordinates.
*
* For the sake of testing, however, we want to specify them relative to the [TwoPane] under test.
*
* Therefore, this method takes in a list of [LocalFoldingFeature]s and the [TwoPane] layout info
* in order to map the [LocalFoldingFeature]s into real [FoldingFeature] with the proper window
* coordinates.
*
* In other words, this allows specifying [LocalFoldingFeature]s as if the [TwoPane] layout matches
* the window bounds.
*/
@OptIn(ExperimentalWindowApi::class)
private fun fakeDisplayFeatures(
density: Density,
twoPaneCoordinates: LayoutCoordinates,
localFoldingFeatures: List
): List {
val boundsTopLeftOffset = twoPaneCoordinates.localToWindow(
twoPaneCoordinates.size.toIntRect().topLeft.toOffset()
)
val boundsBottomRightOffset = twoPaneCoordinates.localToWindow(
twoPaneCoordinates.size.toIntRect().bottomRight.toOffset()
)
val bounds = Rect(
boundsTopLeftOffset,
boundsBottomRightOffset
)
return localFoldingFeatures.map { localFoldingFeature ->
val foldLeft: Float
val foldTop: Float
val foldRight: Float
val foldBottom: Float
with(density) {
if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) {
foldLeft = 0f
foldTop =
(localFoldingFeature.center - localFoldingFeature.size / 2).toPx()
foldRight = twoPaneCoordinates.size.width.toFloat()
foldBottom =
(localFoldingFeature.center + localFoldingFeature.size / 2).toPx()
} else {
foldLeft =
(localFoldingFeature.center - localFoldingFeature.size / 2).toPx()
foldTop = 0f
foldRight =
(localFoldingFeature.center + localFoldingFeature.size / 2).toPx()
foldBottom = twoPaneCoordinates.size.height.toFloat()
}
}
val foldTopLeftOffset = twoPaneCoordinates.localToWindow(
Offset(foldLeft, foldTop)
)
val foldBottomRightOffset = twoPaneCoordinates.localToWindow(
Offset(foldRight, foldBottom)
)
val foldBounds = Rect(
foldTopLeftOffset,
foldBottomRightOffset,
)
val center: Int
val size: Int
if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) {
center = foldBounds.center.y.roundToInt()
size = foldBounds.height.roundToInt()
} else {
center = foldBounds.center.x.roundToInt()
size = foldBounds.width.roundToInt()
}
FoldingFeature(
windowBounds = android.graphics.Rect(
bounds.left.toInt(),
bounds.top.toInt(),
bounds.right.toInt(),
bounds.bottom.toInt()
),
center = center,
size = size,
state = localFoldingFeature.state,
orientation = localFoldingFeature.orientation,
)
}
}
private fun Rect.round(): IntRect =
IntRect(
left = left.roundToInt(),
top = top.roundToInt(),
right = right.roundToInt(),
bottom = bottom.roundToInt()
)
private fun IntRect.toRect(): Rect =
Rect(
left = left.toFloat(),
top = top.toFloat(),
right = right.toFloat(),
bottom = bottom.toFloat()
)
================================================
FILE: adaptive/src/test/resources/robolectric.properties
================================================
# Pin SDK to 30 since Robolectric does not currently support API 31:
# https://github.com/robolectric/robolectric/issues/6635
sdk=30
================================================
FILE: build-logic/convention/build.gradle.kts
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`kotlin-dsl`
}
group = "com.google.accompanist.buildlogic"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
dependencies {
// used by BundleInsideHelper.kt
implementation(libs.apacheAnt)
implementation(libs.shadow)
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.android.tools.common)
compileOnly(libs.compose.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.metalavaGradle)
compileOnly(libs.gradleMavenPublishPlugin)
implementation(libs.truth)
}
tasks {
validatePlugins {
enableStricterValidation = true
failOnWarning = true
}
}
gradlePlugin {
plugins {
register("androidLibrary") {
id = "accompanist.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidLibraryCompose") {
id = "accompanist.android.library.compose"
implementationClass = "AndroidLibraryComposeConventionPlugin"
}
register("androidLint") {
id = "accompanist.android.lint"
implementationClass = "AndroidLintConventionPlugin"
}
register("androidLibraryPublished") {
id = "accompanist.android.library.published"
implementationClass = "AndroidLibraryPublishedConventionPlugin"
}
}
}
================================================
FILE: build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.gradle.LibraryExtension
import com.google.accompanist.configureAndroidCompose
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
class AndroidLibraryComposeConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.library")
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
val extension = extensions.getByType()
configureAndroidCompose(extension)
}
}
}
================================================
FILE: build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.gradle.LibraryExtension
import com.google.accompanist.configureKotlinAndroid
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.kotlin
class AndroidLibraryConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
extensions.configure {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 35
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildFeatures.buildConfig = false
testOptions.animationsDisabled = true
// The resource prefix is derived from the module name,
// so resources inside ":core:module1" must be prefixed with "core_module1_"
resourcePrefix = path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_").lowercase() + "_"
}
dependencies {
add("androidTestImplementation", kotlin("test"))
add("testImplementation", kotlin("test"))
}
}
}
}
================================================
FILE: build-logic/convention/src/main/kotlin/AndroidLibraryPublishedConventionPlugin.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import me.tylerbwong.gradle.metalava.extension.MetalavaExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
class AndroidLibraryPublishedConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply(AndroidLintConventionPlugin::class)
apply("me.tylerbwong.gradle.metalava")
apply("org.jetbrains.dokka")
apply("com.vanniktech.maven.publish")
}
extensions.configure {
sourcePaths.setFrom("src/main")
filename.set("api/current.api")
reportLintsAsErrors.set(true)
}
}
}
}
================================================
FILE: build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.dsl.Lint
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import java.io.File
class AndroidLintConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
when {
pluginManager.hasPlugin("com.android.application") ->
configure { lint(Lint::configure) }
pluginManager.hasPlugin("com.android.library") ->
configure { lint(Lint::configure) }
else -> {
pluginManager.apply("com.android.lint")
configure(Lint::configure)
}
}
}
}
}
private fun Lint.configure() {
textReport = true
textOutput = File("stdout")
// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks
checkReleaseBuilds = false
disable += setOf("GradleOverrides")
}
================================================
FILE: build-logic/convention/src/main/kotlin/com/google/accompanist/AndroidCompose.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
/**
* Configure Compose-specific options
*/
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
buildFeatures {
compose = true
}
testOptions {
unitTests {
// For Robolectric
isIncludeAndroidResources = true
}
}
}
extensions.configure {
fun Provider.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } }
fun Provider<*>.relativeToRootProject(dir: String) = flatMap {
rootProject.layout.buildDirectory.dir(projectDir.toRelativeString(rootDir))
}.map { it.dir(dir) }
project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue()
.relativeToRootProject("compose-metrics")
.let(metricsDestination::set)
project.providers.gradleProperty("enableComposeCompilerReports").onlyIfTrue()
.relativeToRootProject("compose-reports")
.let(reportsDestination::set)
// We include source information to match how the main Compose libraries are shipped.
// This allows accompanist to be displayed properly in the layout inspector and systrace
includeSourceInformation.set(true)
}
}
================================================
FILE: build-logic/convention/src/main/kotlin/com/google/accompanist/BundleInsideHelper.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
import org.apache.tools.zip.ZipOutputStream
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Usage
import org.gradle.api.file.FileTreeElement
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.register
/**
* Originally from https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
* Small modifications based on gradle version
*/
/** Allow java and Android libraries to bundle other projects inside the project jar/aar. */
object BundleInsideHelper {
val CONFIGURATION_NAME = "bundleInside"
val REPACKAGE_TASK_NAME = "repackageBundledJars"
/**
* Creates a configuration for the users to use that will be used to bundle these dependency
* jars inside of libs/ directory inside of the aar.
*
* ```
* dependencies {
* bundleInside(project(":foo"))
* }
* ```
*
* Used project are expected
*
* @param relocations a list of package relocations to apply
* @param dropResourcesWithSuffix used to drop Java resources if they match this suffix, null
* means no filtering
* @receiver the project that should bundle jars specified by this configuration
* @see forInsideAar(String, String)
*/
@JvmStatic
fun Project.forInsideAar(relocations: List?, dropResourcesWithSuffix: String?) {
val bundle = createBundleConfiguration()
val repackage = configureRepackageTaskForType(relocations, bundle, dropResourcesWithSuffix)
// Add to AGP's configuration so this jar get packaged inside of the aar.
dependencies.add("implementation", files(repackage.flatMap { it.archiveFile }))
}
/**
* Creates 3 configurations for the users to use that will be used bundle these dependency jars
* inside of libs/ directory inside of the aar.
*
* ```
* dependencies {
* bundleInside(project(":foo"))
* }
* ```
*
* Used project are expected
*
* @param from specifies from which package the rename should happen
* @param to specifies to which package to put the renamed classes
* @param dropResourcesWithSuffix used to drop Java resources if they match this suffix, null
* means no filtering
* @receiver the project that should bundle jars specified by these configurations
*/
@JvmStatic
fun Project.forInsideAar(from: String, to: String, dropResourcesWithSuffix: String?) {
forInsideAar(listOf(Relocation(from, to)), dropResourcesWithSuffix)
}
/**
* Creates a configuration for users to use that will bundle the dependency jars inside of this
* lint check's jar. This is required because lintPublish does not currently support
* dependencies, so instead we need to bundle any dependencies with the lint jar manually.
* (b/182319899)
*
* ```
* dependencies {
* if (rootProject.hasProperty("android.injected.invoked.from.ide")) {
* compileOnly(LINT_API_LATEST)
* } else {
* compileOnly(LINT_API_MIN)
* }
* compileOnly(KOTLIN_STDLIB)
* // Include this library inside the resulting lint jar
* bundleInside(project(":foo-lint-utils"))
* }
* ```
*
* @receiver the project that should bundle jars specified by these configurations
*/
@JvmStatic
fun Project.forInsideLintJar(): Configuration {
val bundle = createBundleConfiguration()
val compileOnly = configurations.getByName("compileOnly")
val testImplementation = configurations.getByName("testImplementation")
compileOnly.extendsFrom(bundle)
testImplementation.extendsFrom(bundle)
// Relocation needed to avoid classpath conflicts with Android Studio (b/337980250)
// Can be removed if we migrate from using kotlin-metadata-jvm inside of lint checks
val relocations = listOf(Relocation("kotlin.metadata", "androidx.lint.kotlin.metadata"))
val repackage = configureRepackageTaskForType(relocations, bundle, null)
val sourceSets = extensions.getByType(SourceSetContainer::class.java)
repackage.configure {
this.from(sourceSets.findByName("main")?.output)
// kotlin-metadata-jvm has a service descriptor that needs transformation
this.mergeServiceFiles()
// Exclude Kotlin metadata files from kotlin-metadata-jvm
this.exclude(
"META-INF/kotlin-metadata-jvm.kotlin_module",
"META-INF/kotlin-metadata.kotlin_module",
"META-INF/metadata.jvm.kotlin_module",
"META-INF/metadata.kotlin_module"
)
}
listOf("apiElements", "runtimeElements").forEach { config ->
configurations.getByName(config).apply {
outgoing.artifacts.clear()
outgoing.artifact(repackage)
}
}
return bundle
}
data class Relocation(val from: String, val to: String)
private fun Project.configureRepackageTaskForType(
relocations: List?,
configuration: Configuration,
dropResourcesWithSuffix: String?
): TaskProvider {
val action = Action {
configurations = listOf(configuration)
if (relocations != null) {
for (relocation in relocations) {
println("Relocating ${relocation.from} to ${relocation.to}")
relocate(relocation.from, relocation.to)
}
}
val dontIncludeResourceTransformer = DontIncludeResourceTransformer()
dontIncludeResourceTransformer.dropResourcesWithSuffix = dropResourcesWithSuffix
transformers.add(dontIncludeResourceTransformer)
archiveBaseName.set("repackaged")
archiveVersion.set("")
destinationDirectory.set(layout.buildDirectory.dir("repackaged"))
}
return tasks.register(REPACKAGE_TASK_NAME, ShadowJar::class.java, action)
}
private fun Project.createBundleConfiguration(): Configuration {
val bundle =
configurations.create(CONFIGURATION_NAME) {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}
isCanBeConsumed = false
}
return bundle
}
class DontIncludeResourceTransformer : Transformer {
@Optional @Input var dropResourcesWithSuffix: String? = null
override fun getName(): String {
return "DontIncludeResourceTransformer"
}
override fun canTransformResource(element: FileTreeElement?): Boolean {
val path = element?.relativePath?.pathString
return dropResourcesWithSuffix != null &&
(path?.endsWith(dropResourcesWithSuffix!!) == true)
}
override fun transform(context: TransformerContext?) {
// no op
}
override fun hasTransformedResource(): Boolean {
return true
}
override fun modifyOutputStream(zipOutputStream: ZipOutputStream?, b: Boolean) {
// no op
}
}
}
================================================
FILE: build-logic/convention/src/main/kotlin/com/google/accompanist/KotlinAndroid.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.provideDelegate
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension
/**
* Configure base Kotlin with Android options
*/
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
compileSdk = 35
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
configureKotlin()
}
/**
* Configure base Kotlin options
*/
private inline fun Project.configureKotlin() = configure {
// Treat all Kotlin warnings as errors (disabled by default)
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
val warningsAsErrors: String? by project
when (this) {
is KotlinAndroidProjectExtension -> compilerOptions
is KotlinJvmProjectExtension -> compilerOptions
else -> TODO("Unsupported project extension $this ${T::class}")
}.apply {
jvmTarget = JvmTarget.JVM_1_8
allWarningsAsErrors = warningsAsErrors.toBoolean()
explicitApi()
}
}
================================================
FILE: build-logic/convention/src/main/kotlin/com/google/accompanist/ProjectExtensions.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
val Project.libs
get(): VersionCatalog = extensions.getByType().named("libs")
================================================
FILE: build-logic/gradle.properties
================================================
# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
================================================
FILE: build-logic/settings.gradle.kts
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
================================================
FILE: build.gradle
================================================
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.jetbrains.dokka.gradle.DokkaMultiModuleTask
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath libs.affectedmoduledetector
}
}
plugins {
id "com.diffplug.spotless" version "6.5.2"
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.kotlin) apply false
alias(libs.plugins.jetbrains.dokka)
alias(libs.plugins.compose.plugin) apply false
alias(libs.plugins.accompanist.android.library) apply false
alias(libs.plugins.accompanist.android.library.compose) apply false
alias(libs.plugins.accompanist.android.library.published) apply false
alias(libs.plugins.accompanist.android.lint) apply false
alias(libs.plugins.gradle.metalava) apply false
alias(libs.plugins.vanniktech.maven.publish) apply false
}
apply plugin: 'com.dropbox.affectedmoduledetector'
apply plugin: 'com.diffplug.spotless'
tasks.withType(DokkaMultiModuleTask).configureEach {
outputDirectory = rootProject.file('docs/api')
failOnWarning = true
}
affectedModuleDetector {
baseDir = "${project.rootDir}"
pathsAffectingAllModules = [
"gradle/libs.versions.toml",
]
excludedModules = [
"sample"
]
logFilename = "output.log"
logFolder = "${rootProject.buildDir}/affectedModuleDetector"
String baseRef = findProperty("affected_base_ref")
// If we have a base ref to diff against, extract the branch name and use it
if (baseRef != null && !baseRef.isEmpty()) {
// Remove the prefix from the head.
// TODO: need to support other types of git refs
specifiedBranch = baseRef.replace('refs/heads/', '')
compareFrom = "SpecifiedBranchCommit"
} else {
// Otherwise we use the previous commit. This is mostly used for commits to main.
compareFrom = "PreviousCommit"
}
}
subprojects {
apply plugin: 'com.diffplug.spotless'
spotless {
kotlin {
target "**/*.kt"
ktlint(libs.versions.ktlint.get())
licenseHeaderFile rootProject.file('spotless/copyright.txt')
}
groovyGradle {
target '**/*.gradle'
greclipse().configFile(rootProject.file('spotless/greclipse.properties'))
licenseHeaderFile rootProject.file('spotless/copyright.txt'),
'(buildscript|apply|import|plugins)'
}
}
// Remove all test apps after running UI tests.
// This is specially important in CI so that test emulators don't run out of space.
tasks.whenTaskAdded { task ->
if (task.name == 'connectedDebugAndroidTest') {
task.finalizedBy 'uninstallDebugAndroidTest'
}
}
configurations.configureEach {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
// Make sure that we're using the Android version of Guava
if (details.requested.group == 'com.google.guava'
&& details.requested.module.name == 'guava'
&& details.requested.version.contains('jre')) {
details.useVersion details.requested.version.replace('jre', 'android')
}
}
}
// Read in the signing.properties file if it is exists
def signingPropsFile = rootProject.file('release/signing.properties')
if (signingPropsFile.exists()) {
def localProperties = new Properties()
signingPropsFile.withInputStream { is -> localProperties.load(is) }
localProperties.each { prop ->
if (prop.key == "signing.secretKeyRingFile") {
// If this is the key ring, treat it as a relative path
project.ext.set(prop.key, rootProject.file(prop.value).absolutePath)
} else {
project.ext.set(prop.key, prop.value)
}
}
}
// Must be afterEvaluate or else com.vanniktech.maven.publish will overwrite our
// dokka and version configuration.
afterEvaluate {
if (tasks.findByName('dokkaHtmlPartial') == null) {
// If dokka isn't enabled on this module, skip
return
}
tasks.named('dokkaHtmlPartial') {
dokkaSourceSets.configureEach {
reportUndocumented.set(true)
skipEmptyPackages.set(true)
skipDeprecated.set(true)
jdkVersion.set(8)
// Add Android SDK packages
noAndroidSdkLink.set(false)
// Add samples from :sample module
samples.from(rootProject.file("sample/src/main/java/"))
// AndroidX + Compose docs
externalDocumentationLink {
url.set(new URL("https://developer.android.com/reference/"))
packageListUrl.set(new URL("https://developer.android.com/reference/androidx/package-list"))
}
externalDocumentationLink {
url.set(new URL("https://developer.android.com/reference/kotlin/"))
packageListUrl.set(new URL("https://developer.android.com/reference/kotlin/androidx/package-list"))
}
sourceLink {
localDirectory.set(project.file("src/main/java"))
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(new URL("https://github.com/google/accompanist/blob/main/${project.name}/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
}
}
}
afterEvaluate {
def composeSnapshot = libs.versions.composesnapshot.get()
if (composeSnapshot.length() > 1) {
// We're depending on a Jetpack Compose snapshot, update the library version name
// to indicate it's from a Compose snapshot
def versionName = project.properties.get('VERSION_NAME')
if (versionName.contains("SNAPSHOT")) {
version = versionName.replace('-SNAPSHOT', ".compose-${composeSnapshot}-SNAPSHOT")
}
}
if (!version.endsWith('SNAPSHOT')) {
// If we're not a SNAPSHOT library version, we fail the build if we're relying on
// any snapshot dependencies
configurations.configureEach { configuration ->
configuration.dependencies.configureEach { dependency ->
if (dependency instanceof ProjectDependency) {
// We don't care about internal project dependencies
return
}
def depVersion = dependency.version
if (depVersion != null && depVersion.endsWith('SNAPSHOT')) {
throw new IllegalArgumentException(
"Using SNAPSHOT dependency with non-SNAPSHOT library version: $dependency"
)
}
}
}
}
}
}
================================================
FILE: checksum.sh
================================================
#!/bin/bash
# Copyright 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
RESULT_FILE=$1
if [ -f $RESULT_FILE ]; then
rm $RESULT_FILE
fi
touch $RESULT_FILE
checksum_file() {
echo $(openssl md5 $1 | awk '{print $2}')
}
FILES=()
while read -r -d ''; do
FILES+=("$REPLY")
done < <(find . -type f \( -name "build.gradle*" -o -name "*.versions.toml" -o -name "gradle-wrapper.properties" \) -print0)
# Loop through files and append MD5 to result file
for FILE in ${FILES[@]}; do
echo $(checksum_file $FILE) >> $RESULT_FILE
done
# Now sort the file so that it is idempotent
sort $RESULT_FILE -o $RESULT_FILE
================================================
FILE: docs/adaptive.md
================================================
# Adaptive utilities for Jetpack Compose
[](https://search.maven.org/search?q=g:com.google.accompanist)
A library providing a collection of utilities for adaptive layouts.
## calculateDisplayFeatures
[`calculateDisplayFeatures(activity)`](../api/adaptive/com.google.accompanist.adaptive/calculate-display-features.html) returns the current list of `DisplayFeature`s,
as reported by the [Jetpack WindowManager library](https://developer.android.com/jetpack/androidx/releases/window).
These contain the list of folds (if any), and can be used to drive components like [`TwoPane`](#TwoPane).
## TwoPane
[`TwoPane`](../api/adaptive/com.google.accompanist.adaptive/-two-pane.html) is a UI component that positions exactly two slots on the screen.
The default positioning of these two slots is driven by a [`TwoPaneStrategy`](../api/adaptive/com.google.accompanist.adaptive/-two-pane-strategy.html),
which can decide to orient the two slots side-by-side horizontally or vertically, and also configure the gap between them.
The built-in [`HorizontalTwoPaneStrategy`](../api/adaptive/com.google.accompanist.adaptive/-horizontal-two-pane-strategy.html) and
[`VerticalTwoPaneStrategy`](../api/adaptive/com.google.accompanist.adaptive/-vertical-two-pane-strategy.html) allow positioning the
slots based on a fixed offset, or as some fraction of the space.
[`TwoPane`](../api/adaptive/com.google.accompanist.adaptive/-two-pane.html) also requires a list of display features (to be retrieved with [`calculateDisplayFeatures`](#calculateDisplayFeatures)),
and optionally a [`FoldAwareConfiguration`](../api/adaptive/com.google.accompanist.adaptive/-fold-aware-configuration.html) to determine which folds to handle automatically.
When there is a fold that intersects with the [`TwoPane`](../api/adaptive/com.google.accompanist.adaptive/-two-pane.html) component that is obscuring or separating,
the [`TwoPane`](../api/adaptive/com.google.accompanist.adaptive/-two-pane.html) will automatically place the slots to avoid the fold.
When there is no fold, the default supplied strategy will be used instead.
## FoldAwareColumn
[`FoldAwareColumn`](../api/adaptive/com.google.accompanist.adaptive/-fold-aware-column.html) is a simplified version of [Column](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#Column(androidx.compose.ui.Modifier,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.ui.Alignment.Horizontal,kotlin.Function1)) that places children in a fold-aware manner.
[`FoldAwareColumn`](../api/adaptive/com.google.accompanist.adaptive/-fold-aware-column.html) requires a list of display features (to be retrieved with [`calculateDisplayFeatures`](#calculatedisplayfeatures)) to determine which folds to handle automatically.
The built-in `foldPadding` parameter is zero, and the values of the vertical padding are used in the layout determine how much space should be left around a fold when placing children.
When there is a horizontal fold that is obscuring or separating, the layout will begin placing children from the top of the available space. If a child is projected to overlap the fold, then its y-coordinate is increased so it will be placed fully below the fold, as will any other remaining children.
When there is no fold, the children will be placed consecutively with no y-coordinate adjustments.
Optionally, children can be modified with the `ignoreFold()` attribute, which means that they will be placed as if no fold is present even if they overlap a fold.
## Download
[](https://search.maven.org/search?q=g:com.google.accompanist)
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-adaptive:"
}
```
================================================
FILE: docs/appcompat-theme.md
================================================
# AppCompat Compose Theme Adapter
[](https://search.maven.org/search?q=g:com.google.accompanist)
!!! warning
**This library is deprecated in favor of the new [`themeadapter-appcompat`][themeadapterappcompatlib] artifact.** The migration guide and original documentation is below.
## Migration
Accompanist AppCompat Theme Adapter has moved from the [`appcompat-theme`][appcompatthemelib] artifact to the [`themeadapter-appcompat`][themeadapterappcompatlib] artifact.
The implementation is identical but the dependency and import package have changed.
### Migration steps
1. Change the dependency from `com.google.accompanist:accompanist-appcompat-theme:` to `com.google.accompanist:accompanist-themeadapter-appcompat:`
2. Change any `com.google.accompanist.appcompattheme.*` imports to `com.google.accompanist.themeadapter.appcompat.*`
## Original Docs
A library that enables reuse of [AppCompat][appcompat] XML themes for theming in [Jetpack Compose][compose].
The basis of theming in [Jetpack Compose][compose] is the [`MaterialTheme`][materialtheme] composable, where you provide [`Colors`][colors], [`Shapes`][shapes] and [`Typography`][typography] instances containing your styling parameters:
``` kotlin
MaterialTheme(
typography = type,
colors = colors,
shapes = shapes
) {
// Surface, Scaffold, etc
}
```
[AppCompat][appcompat] XML themes allow for similar but coarser theming via XML theme attributes, like so:
``` xml
```
This library attempts to bridge the gap between [AppCompat][appcompat] XML themes, and themes in [Jetpack Compose][compose], allowing your composable [`MaterialTheme`][materialtheme] to be based on the `Activity`'s XML theme:
``` kotlin
AppCompatTheme {
// MaterialTheme.colors, MaterialTheme.shapes, MaterialTheme.typography
// will now contain copies of the context's theme
}
```
This is especially handy when you're migrating an existing app, a fragment (or other UI container) at a time.
!!! caution
If you are using [Material Design Components](https://material.io/develop/android/) in your app, you should use the
[MDC Compose Theme Adapter](https://github.com/material-components/material-components-android-compose-theme-adapter)
instead, as it allows much finer-grained reading of your theme.
### Customizing the theme
The [`AppCompatTheme()`][appcompattheme] function will automatically read the host context's AppCompat theme and pass them to [`MaterialTheme`][materialtheme] on your behalf, but if you want to customize the generated values, you can do so via the [`createAppCompatTheme()`][createappcompattheme] function:
``` kotlin
val context = LocalContext.current
var (colors, type) = context.createAppCompatTheme()
// Modify colors or type as required. Then pass them
// through to MaterialTheme...
MaterialTheme(
colors = colors,
typography = type
) {
// rest of layout
}
```
## Generated theme
Synthesizing a material theme from a `Theme.AppCompat` theme is not perfect, since `Theme.AppCompat`
does not expose the same level of customization as is available in material theming.
Going through the pillars of material theming:
### Colors
AppCompat has a limited set of top-level color attributes, which means that [`AppCompatTheme()`][appcompattheme]
has to generate/select alternative colors in certain situations. The mapping is currently:
| MaterialTheme color | AppCompat attribute |
|---------------------|-------------------------------------------------------|
| primary | `colorPrimary` |
| primaryVariant | `colorPrimaryDark` |
| onPrimary | Calculated black/white |
| secondary | `colorAccent` |
| secondaryVariant | `colorAccent` |
| onSecondary | Calculated black/white |
| surface | Default |
| onSurface | `android:textColorPrimary`, else calculated black/white |
| background | `android:colorBackground` |
| onBackground | `android:textColorPrimary`, else calculated black/white |
| error | `colorError` |
| onError | Calculated black/white |
Where the table says "calculated black/white", this means either black/white, depending on
which provides the greatest contrast against the corresponding background color.
### Typography
AppCompat does not provide any semantic text appearances (such as headline6, body1, etc), and
instead relies on text appearances for specific widgets or use cases. As such, the only thing
we read from an AppCompat theme is the default `app:fontFamily` or `android:fontFamily`.
For example:
``` xml
```
Compose does not currently support downloadable fonts, so any font referenced from the theme
should from your resources. See [here](https://developer.android.com/guide/topics/resources/font-resource)
for more information.
### Shape
AppCompat has no concept of shape theming, therefore we use the default value from
[`MaterialTheme.shapes`][shapes]. If you wish to provide custom values, use the `shapes` parameter on `AppCompatTheme`.
## Limitations
There are some known limitations with the implementation at the moment:
* This relies on your `Activity`/`Context` theme extending one of the `Theme.AppCompat` themes.
* Variable fonts are not supported in Compose yet, meaning that the value of `android:fontVariationSettings` are currently ignored.
* You can modify the resulting `MaterialTheme` in Compose as required, but this _only_ works in Compose. Any changes you make will not be reflected in the Activity theme.
---
## Usage
[](https://search.maven.org/search?q=g:com.google.accompanist)
``` groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-appcompat-theme:"
}
```
### Library Snapshots
Snapshots of the current development version of this library are available, which track the latest commit. See [here](../using-snapshot-version) for more information on how to use them.
---
## Contributions
Please contribute! We will gladly review any pull requests.
Make sure to read the [Contributing](../contributing) page first though.
## License
```
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
[appcompatthemelib]: ../appcompat-theme
[themeadapterappcompatlib]: ../themeadapter-appcompat
[compose]: https://developer.android.com/jetpack/compose
[appcompat]: https://developer.android.com/jetpack/androidx/releases/appcompat
[appcompattheme]: ../api/appcompat-theme/appcompat-theme/com.google.accompanist.appcompattheme/-app-compat-theme.html
[createappcompattheme]: ../api/appcompat-theme/appcompat-theme/com.google.accompanist.appcompattheme/create-app-compat-theme.html
[materialtheme]: https://developer.android.com/reference/kotlin/androidx/compose/material/MaterialTheme
[shapes]: https://developer.android.com/reference/kotlin/androidx/compose/material/Shapes
[colors]: https://developer.android.com/reference/kotlin/androidx/compose/material/Colors
[typography]: https://developer.android.com/reference/kotlin/androidx/compose/material/Typography
================================================
FILE: docs/drawablepainter.md
================================================
# Drawable Painter
[](https://search.maven.org/search?q=g:com.google.accompanist)
A library which provides a way to use Android [drawables](https://developer.android.com/guide/topics/resources/drawable-resource) as Jetpack Compose [Painters](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/painter/Painter).
This library attempts to support most Drawable configuration, as well as [Animatable](https://developer.android.com/reference/android/graphics/drawable/Animatable) drawables, such as [AnimatedVectorDrawable](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable).
## Usage
``` kotlin
@Composable
fun DrawDrawable() {
val drawable = AppCompatResources.getDrawable(LocalContext.current, R.drawable.rectangle)
Image(
painter = rememberDrawablePainter(drawable = drawable),
contentDescription = "content description",
)
}
```
## Download
[](https://search.maven.org/search?q=g:com.google.accompanist)
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-drawablepainter:"
}
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-drawablepainter/
================================================
FILE: docs/migration.md
================================================
# Migration from dev.chrisbanes.accompanist
In March 2021, the Accompanist project moved from [github.com/chrisbanes/accompanist](https://github.com/chrisbanes/accompanist) to [github.com/google/accompanist](https://github.com/google/accompanist). At the same time we migrated the libraries over to a new package name and Maven group ID.
As a summary:
- All code was refactored from the `dev.chrisbanes.accompanist` root package to `com.google.accompanist` package.
- The Maven group ID was changed from `dev.chrisbanes.accompanist` to `com.google.accompanist`.
## Semi-automatic migration...
The following methods below are available for your information only, but may help if you need to migrate from the old package name.
!!! warning
Use these at your own risk, but they have worked on multiple projects from my testing. It's a good idea to make sure that you've made a backup or committed any changes before running these.
### Android Studio / IntelliJ
You can use the [Replace in Path](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html#replace_search_string_in_project) pane (⇧⌘R on Mac) in Android Studio to do a project-wide search and replace.

- Find query: `dev.chrisbanes.accompanist`
- Replace string: `com.google.accompanist`
- _Optional:_ Set the file mask to `*.kt` so that only Kotlin files are searched. Repeat for `*.gradle`.
Similar can be achieved in [Visual Studio Code](https://code.visualstudio.com/docs/editor/codebasics#_search-across-files). Other IDEs / text editors are available.
### YOLO commands
These commands while automatically replace any imports and Gradle dependencies for the project in the current directory.
#### MacOS
``` bash
find . -type f \( -name '*.kt' -or -name '*.gradle*' \) \
-exec sed -i '' 's/dev\.chrisbanes\.accompanist/com\.google\.accompanist/' {} \;
```
#### Linux
``` bash
find . -type f \( -name '*.kt' -or -name '*.gradle*' \) \
-exec sed -i 's/dev\.chrisbanes\.accompanist/com\.google\.accompanist/' {} \;
```
================================================
FILE: docs/navigation-animation.md
================================================
# Jetpack Navigation Compose Animation
[](https://search.maven.org/search?q=g:com.google.accompanist)
A library which provides [Compose Animation](https://developer.android.com/jetpack/compose/animation) support for [Jetpack Navigation Compose](https://developer.android.com/jetpack/compose/navigation).
!!! warning
**This library is deprecated, with official navigation-compose support in [androidx.navigation.compose](https://developer.android.com/jetpack/compose/navigation).** The original documentation is below the migration guide.
## Migration
The official `androidx.navigation.compose` version 2.7.0-alpha01+ offers all of the same functionality as Accompanist Navigation Animation.
1. Make sure you are using Compose 1.5.0-beta01+ before migrating to `androidx.navigation.compose`.
2. Replace dependency `com.google.accompanist:accompanist-navigation-animation:` with `androidx.navigation:navigation-compose:`
3. Replace `rememberAnimatedNavController` with `rememberNavController` and change import to `androidx.navigation.compose.rememberNavController`
4. Replace `AnimatedNavHost` with `NavHost` and change import to `androidx.navigation.compose.NavHost`
5. Replace `AnimatedComposeNavigator` with `ComposeNavigator` and change import to `androidx.navigation.compose.ComposeNavigator`
6. Replace `AnimatedComposeNavigator()` constructor with `ComposeNavigator()` constructor
7. Replace `AnimatedComposeNavigator.Destination` with `ComposeNavigator.Destination`
8. Change import for composable from `com.google.accompanist.navigation.animation.composable` to `androidx.navigation.compose.composable`
9. Change import for navigation from `com.google.accompanist.navigation.animation.navigation` to `androidx.navigation.compose.navigation`
## Migration Table
The following is a mapping of Navigation classes and functions from accompanist to androidx.compose:
| accompanist navigation-animation | androidx.navigation.compose |
|----------------------------------------|--------------------------------|
| `AnimatedNavHost` | `NavHost` |
| `AnimatedComposeNavigator` | `ComposeNavigator` |
| `AnimatedComposeNavigator.Destination` | `ComposeNavigator.Destination` |
| `rememberAnimatedNavController()` | `rememberNavController()` |
| `NavGraphBuilder.composable()` | `NavGraphBuilder.composable()` |
| `NavGraphBuilder.navigation()` | `NavGraphBuilder.navigation()` |
Of note, ComposeNavigation.Destination allows use of `AnimatedContentScope` instead of just `AnimatedVisibilityScope`.
# Deprecated Guidance for Accompanist Navigation
The following is the deprecated guide for using Navigation in Accompanist. Please see above migration section for how to use the `androidx.navigation.compose` Navigation.
## Download
[](https://search.maven.org/search?q=g:com.google.accompanist)
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-navigation-animation:"
}
```
Follow the steps below to either [add](#usage) Jetpack Navigation Compose to your app, or to [migrate](#migration) an existing Jetpack Navigation Compose implementation.
## Usage
The `AnimatedNavHost` composable offers a way to add custom transitions to composables in
Navigation Compose via parameters that can be attached to either an individual `composable`
destination, a `navigation` element, or to the `AnimatedNavHost` itself.
Each lambda has an [`AnimatedContentScope`](https://developer.android.com/reference/kotlin/androidx/compose/animation/AnimatedContentScope) receiver scope that allows you to use special transitions (such as [`slideIntoContainer`](https://developer.android.com/reference/kotlin/androidx/compose/animation/AnimatedContentScope#slideIntoContainer(androidx.compose.animation.AnimatedContentScope.SlideDirection,androidx.compose.animation.core.FiniteAnimationSpec,kotlin.Function1)) and [`slideOutOfContainer`](https://developer.android.com/reference/kotlin/androidx/compose/animation/AnimatedContentScope#slideOutOfContainer(androidx.compose.animation.AnimatedContentScope.SlideDirection,androidx.compose.animation.core.FiniteAnimationSpec,kotlin.Function1))) and gives you access to the [`initialState`](https://developer.android.com/reference/kotlin/androidx/compose/animation/AnimatedContentScope#initialState()) and [`targetState`](https://developer.android.com/reference/kotlin/androidx/compose/animation/AnimatedContentScope#targetState()) properties that let you customize what transitions are run based on what screen you are transitioning from (the `initialState`) and transitioning to (the `targetState`).
- `enterTransition` controls what [`EnterTransition`](https://developer.android.com/reference/kotlin/androidx/compose/animation/EnterTransition.html) is run when the `targetState` `NavBackStackEntry` is appearing on the screen.
- `exitTransition` controls what [`ExitTransition`](https://developer.android.com/reference/kotlin/androidx/compose/animation/ExitTransition) is run when the `initialState` `NavBackStackEntry` is disappearing from the screen.
- `popEnterTransition` defaults to `enterTransition`, but can be overridden to provide a separate [`EnterTransition`](https://developer.android.com/reference/kotlin/androidx/compose/animation/EnterTransition.html) when the `targetState` `NavBackStackEntry` is appearing on the screen due to a pop operation (i.e., `popBackStack()`).
- `popExitTransition` defaults to `exitTransition`, but can be overridden to provide a separate [`ExitTransition`](https://developer.android.com/reference/kotlin/androidx/compose/animation/ExitTransition) when the `initialState` `NavBackStackEntry` is disappearing from the screen due to a pop operation (i.e., `popBackStack()`).
For each transition, if a `composable` destination returns `null`, the parent `navigation` element's transition will be used, thus allowing you to set a global set of transitions at the navigation graph level that will apply to every `composable` in that graph. This continues up the hierarchy until you reach the root `AnimatedNavHost`, which controls the global transitions for all destinations and nested graphs that do not specify one.
Note: this means that if a destination wants to instantly jump cut between destinations, it should return [`EnterTransition.None`](https://developer.android.com/reference/kotlin/androidx/compose/animation/EnterTransition#None()) or [`ExitTransition.None`](https://developer.android.com/reference/kotlin/androidx/compose/animation/ExitTransition#None()) to signify that no transition should be run, rather than return `null`.
```kotlin
@Composable
private fun ExperimentalAnimationNav() {
val navController = rememberAnimatedNavController()
AnimatedNavHost(navController, startDestination = "Blue") {
composable(
"Blue",
enterTransition = {
when (initialState.destination.route) {
"Red" ->
slideIntoContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
"Red" ->
slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
"Red" ->
slideIntoContainer(AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
"Red" ->
slideOutOfContainer(AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) { BlueScreen(navController) }
composable(
"Red",
enterTransition = {
when (initialState.destination.route) {
"Blue" ->
slideIntoContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
"Blue" ->
slideOutOfContainer(AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
"Blue" ->
slideIntoContainer(AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
"Blue" ->
slideOutOfContainer(AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) { RedScreen(navController) }
}
}
```
For more examples, refer to the [samples](https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/navigation/animation).
## Migration
To migrate from using the Navigation Compose APIs do the following:
* Replace `rememberNavController()` with `rememberAnimatedNavController()`
* Replace `NavHost` with `AnimatedNavHost`
* Replace `import androidx.navigation.compose.navigation` with `import com.google.accompanist.navigation.animation.navigation`
* Replace `import androidx.navigation.compose.composable` with `import com.google.accompanist.navigation.animation.composable`
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-navigation-animation/
For more details see [Animations in Navigation Compose](https://medium.com/androiddevelopers/animations-in-navigation-compose-36d48870776b)
================================================
FILE: docs/navigation-material.md
================================================
# Jetpack Navigation Compose Material
[](https://search.maven.org/search?q=g:com.google.accompanist)
A library which provides [Compose Material](https://developer.android.com/jetpack/androidx/releases/compose-material) support for [Jetpack Navigation Compose](https://developer.android.com/jetpack/compose/navigation).
This features composable bottom sheet destinations.
!!! warning
**This library is deprecated, with official material-navigation support in [androidx.compose.material.navigation](https://developer.android.com/jetpack/androidx/releases/compose-material#1.7.0-alpha04).** The original documentation is below the migration guide.
## Migration
The official `androidx.compose.material.navigation` version 1.7.0-alpha04+ offers all of the same functionality as Accompanist Navigation Material.
All class names are the same, the only needed changes are import related.
1. Replace dependency `com.google.accompanist:accompanist-navigation-material:` with `androidx.compose.material:material-navigation:`
2. Change import for ModalBottomSheetLayout from `com.google.accompanist.navigation.material.ModalBottomSheetLayout` to `androidx.compose.material.navigation.ModalBottomSheetLayout`
3. Change import for bottomSheet from `com.google.accompanist.navigation.material.bottomSheet` to `androidx.compose.material.navigation.bottomSheet`
4. Change import for rememberBottomSheetNavigator from `com.google.accompanist.navigation.material.rememberBottomSheetNavigator` to `androidx.compose.material.navigation.rememberBottomSheetNavigator`
5. Change import for BottomSheetNavigator from `com.google.accompanist.navigation.material.BottomSheetNavigator` to `androidx.compose.material.navigation.BottomSheetNavigator`
6. Change import for BottomSheetNavigatorSheetState from `com.google.accompanist.navigation.material.BottomSheetNavigatorSheetState` to `androidx.compose.material.navigation.BottomSheetNavigatorSheetState`
# Deprecated Guidance for Accompanist Navigation Material
The following is the deprecated guide for using Navigation Material in Accompanist. Please see above migration section for how to use the `androidx.compose.material.navigation` Material Navigation.
## Usage
### Bottom Sheet Destinations
1. Create a `BottomSheetNavigator` and add it to the `NavController`:
```kotlin
@Composable
fun MyApp() {
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
}
```
2. Wrap your `NavHost` in the `ModalBottomSheetLayout` composable that accepts a `BottomSheetNavigator`.
```kotlin
@Composable
fun MyApp() {
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
ModalBottomSheetLayout(bottomSheetNavigator) {
NavHost(navController, "home") {
// We'll define our graph here in a bit!
}
}
}
```
3. Register a bottom sheet destination
```kotlin
@Composable
fun MyApp() {
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
ModalBottomSheetLayout(bottomSheetNavigator) {
NavHost(navController, "home") {
composable(route = "home") {
...
}
bottomSheet(route = "sheet") {
Text("This is a cool bottom sheet!")
}
}
}
}
```
For more examples, refer to the [samples](https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/navigation/material).
## Download
[](https://search.maven.org/search?q=g:com.google.accompanist)
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-navigation-material:"
}
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-navigation-material/
================================================
FILE: docs/permissions.md
================================================
# Jetpack Compose Permissions
[](https://search.maven.org/search?q=g:com.google.accompanist)
A library which provides [Android runtime permissions](https://developer.android.com/guide/topics/permissions/overview) support for Jetpack Compose.
!!! warning
The permission APIs are currently experimental and they could change at any time.
All of the APIs are marked with the `@ExperimentalPermissionsApi` annotation.
## Usage
### `rememberPermissionState` and `rememberMultiplePermissionsState` APIs
The `rememberPermissionState(permission: String)` API allows you to request a certain permission
to the user and check for the status of the permission.
`rememberMultiplePermissionsState(permissions: List)` offers the same but for multiple
permissions at the same time.
Both APIs expose properties for you to follow the workflow as described in the
[permissions documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
!!! caution
The call to the method that requests the permission to the user (e.g. `PermissionState.launchPermissionRequest()`)
needs to be invoked from a non-composable scope. For example, from a side-effect or from a
non-composable callback such as a `Button`'s `onClick` lambda.
The following code exercises the [permission request workflow](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
```kotlin
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun FeatureThatRequiresCameraPermission() {
// Camera permission state
val cameraPermissionState = rememberPermissionState(
android.Manifest.permission.CAMERA
)
if (cameraPermissionState.status.isGranted) {
Text("Camera permission Granted")
} else {
Column {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The camera is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Camera permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
```
For more examples, refer to the [samples](https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/permissions).
## Limitations
This permissions wrapper is built on top of the available Android platform APIs. We cannot extend
the platform's capabilities. For example, it's not possible to differentiate between the
_it's the first time requesting the permission_ vs _the user doesn't want to be asked again_
use cases.
## Download
[](https://search.maven.org/search?q=g:com.google.accompanist)
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-permissions:"
}
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-permissions/
================================================
FILE: docs/systemuicontroller.md
================================================
# System UI Controller for Jetpack Compose
[](https://search.maven.org/search?q=g:com.google.accompanist)
!!! warning
**This library is deprecated, and the API is no longer maintained. We recommend forking the implementation and customising it to your needs.** The original documentation is below.
## Migration
Recommendation: If you were using SystemUIController to go edge-to-edge in your activity and change the system bar colors and system bar icon colors, use the new [Activity.enableEdgeToEdge](https://developer.android.com/reference/androidx/activity/ComponentActivity#(androidx.activity.ComponentActivity).enableEdgeToEdge(androidx.activity.SystemBarStyle,androidx.activity.SystemBarStyle)) method available in androidx.activity 1.8.0-alpha03 and later. This method backports the scrims used on some versions of Android. [This](https://github.com/android/nowinandroid/pull/817) is a sample PR of the migration to the new method and removing the dependency on SystemUIController in Now in Android.
For other usages, migrate to using WindowInsetsControllerCompat or window APIs directly.
## Original Documentation
System UI Controller provides easy-to-use utilities for updating the System UI bar colors within Jetpack Compose.
## Usage
To control the system UI in your composables, you need to get a [`SystemUiController`](../api/systemuicontroller/systemuicontroller/com.google.accompanist.systemuicontroller/-system-ui-controller/) instance. The library provides the [`rememberSystemUiController()`](../api/systemuicontroller/systemuicontroller/com.google.accompanist.systemuicontroller/remember-system-ui-controller.html) function which returns an instance for the current system (currently only Android).
In your layouts you can update the system bar colors like so:
``` kotlin
// Remember a SystemUiController
val systemUiController = rememberSystemUiController()
val useDarkIcons = !isSystemInDarkTheme()
DisposableEffect(systemUiController, useDarkIcons) {
// Update all of the system bar colors to be transparent, and use
// dark icons if we're in light theme
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = useDarkIcons
)
// setStatusBarColor() and setNavigationBarColor() also exist
onDispose {}
}
```
## System bar icon colors
The library automatically handles API level differences when running on Android devices. If we look at the example
of status bar icons, Android only natively supports dark icons on API 23+. This library handles this by automatically
altering the requested color with a scrim, to maintain contrast:

Similar happens on navigation bar color, which is only available on API 26+.
### Modifying scrim logic
The scrim logic can be modified if needed:
``` kotlin
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = true
) { requestedColor ->
// TODO: return a darkened color to be used when the system doesn't
// natively support dark icons
}
```
## Samples
For complete samples, check out the [Insets samples](https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/insets) which all use `SystemUiController` to set transparent system bars.
## Download
[](https://search.maven.org/search?q=g:com.google.accompanist)
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-systemuicontroller:"
}
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-systemuicontroller/
================================================
FILE: docs/updating.md
================================================
# Updating & releasing Accompanist
This doc is mostly for maintainers.
## New features & bugfixes
All new features should be uploaded as PRs against the `main` branch.
Once merged into `main`, they will be automatically merged into the `snapshot` branch.
## Jetpack Compose Snapshots
We publish snapshot versions of Accompanist, which depend on a `SNAPSHOT` versions of Jetpack Compose. These are built from the `snapshot` branch.
### Updating to a newer Compose snapshot
As mentioned above, updating to a new Compose snapshot is done by submitting a new PR against the `snapshot` branch:
``` sh
git checkout snapshot && git pull
# Create branch for PR
git checkout -b update_snapshot
```
Now edit the project to depend on the new Compose SNAPSHOT version:
Edit [`/gradle/libs.versions.toml`](https://github.com/google/accompanist/blob/main/gradle/libs.versions.toml):
Under `[versions]`:
1. Update the `composesnapshot` property to be the snapshot number
2. Ensure that the `compose` property is correct
Make sure the project builds and test pass:
```
./gradlew check
```
Now `git commit` the changes and push to GitHub.
Finally create a PR (with the base branch as `snapshot`) and send for review.
## Releasing
Once the next Jetpack Compose version is out, we're ready to push a new release:
### #1: Merge `snapshot` into `main`
First we merge the `snapshot` branch into `main`:
``` sh
git checkout snapshot && git pull
git checkout main && git pull
# Create branch for PR
git checkout -b main_snapshot_merge
# Merge in the snapshot branch
git merge snapshot
```
### #2: Update dependencies
Edit [`/gradle/libs.versions.toml`](https://github.com/google/accompanist/blob/main/gradle/libs.versions.toml):
Under `[versions]`:
1. Update the `composesnapshot` property to a single character (usually `-`). This disables the snapshot repository.
2. Update the `compose` property to match the new release (i.e. `1.0.0-beta06`)
Make sure the project builds and test pass:
```
./gradlew check
```
Commit the changes.
### #3: Bump the version number
Edit [gradle.properties](https://github.com/google/accompanist/blob/main/gradle.properties):
* Update the `VERSION_NAME` property and remove the `-SNAPSHOT` suffix.
Commit the changes, using the commit message containing the new version name.
### #4: Push to GitHub
Push the branch to GitHub and create a PR against the `main` branch, and send for review. Once approved and merged, it will be automatically deployed to Maven Central.
### #5: Create release
Once the above PR has been approved and merged, we need to create the GitHub release:
* Open up the [Releases](https://github.com/google/accompanist/releases) page.
* At the top you should see a 'Draft' release, auto populated with any PRs since the last release. Click 'Edit'.
* Make sure that the version number matches what we released (the tool guesses but is not always correct).
* Double check everything, then press 'Publish release'.
At this point the release is published. This will trigger the docs action to run, which will auto-deploy a new version of the [website](https://google.github.io/accompanist/).
### #6: Prepare the next development version
The current release is now finished, but we need to update the version for the next development version:
Edit [gradle.properties](https://github.com/google/accompanist/blob/main/gradle.properties):
* Update the `VERSION_NAME` property, by increasing the version number, and adding the `-SNAPSHOT` suffix.
* Example: released version: `0.3.0`. Update to `0.3.1-SNAPSHOT`
`git commit` and push to `main`.
Finally, merge all of these changes back to `snapshot`:
```
git checkout snapshot && git pull
git merge main
git push
```
================================================
FILE: docs/using-snapshot-version.md
================================================
# Using a Snapshot Version of the Library
If you would like to depend on the cutting edge version of the Accompanist
library, you can use the [snapshot versions][snap] that are published to
[Sonatype OSSRH](https://central.sonatype.org/)'s snapshot repository. These are updated on every commit to `main`.
To do so:
```groovy
repositories {
// ...
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
}
dependencies {
// Check the latest SNAPSHOT version from the link above
classpath 'com.google.accompanist:accompanist-coil:XXX-SNAPSHOT'
}
```
You might see a number of different versioned snapshots. If we use an example:
* `0.3.0-SNAPSHOT` is a build from the `main` branch, and depends on the latest tagged Jetpack Compose release (i.e. [alpha03](https://developer.android.com/jetpack/androidx/releases/compose#1.0.0-alpha03)).
* `0.3.0.compose-6574163-SNAPSHOT` is a build from the `snapshot` branch. This depends on the [SNAPSHOT build](https://androidx.dev) of Jetpack Compose from build `6574163`. You should only use these if you are using Jetpack Compose snapshot versions (see below).
### Using Jetpack Compose Snapshots
If you're using [`SNAPSHOT`](https://androidx.dev) versions of the `androidx.compose` libraries, you might run into issues with the current stable Accompanist release forcing an older version of those libraries.
We publish snapshot versions of Accompanist which depend on recent Jetpack Compose SNAPSHOT repositories. To find a recent build, look through the [snapshot repository][snap] for any versions in the scheme `x.x.x.compose-YYYY-SNAPSHOT` (for example: `0.3.0.compose-6574163-SNAPSHOT`). The `YYYY` in the scheme is the snapshot build being used from [AndroidX](https://androidx.dev) (from the example: build [`6574163`](https://androidx.dev/snapshots/builds/6574163/artifacts)). You can then use it like so:
``` groovy
repositories {
// ...
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
}
dependencies {
// Check the latest SNAPSHOT version from the link above
classpath 'com.google.accompanist:accompanist-coil:XXXX.compose-YYYYY-SNAPSHOT'
}
```
These builds are updated regularly, but there's no guarantee that we will create one for a given snapshot number.
*Note:* you might also see versions in the scheme `x.x.x.ui-YYYY-SNAPSHOT`. These are the same, just using an older suffix.
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/
================================================
FILE: docs/web.md
================================================
# WebView wrapper for Jetpack Compose
[](https://search.maven.org/search?q=g:com.google.accompanist)
A library which provides a Jetpack Compose wrapper around Android's WebView.
!!! warning
**This library is deprecated, and the API is no longer maintained. We recommend forking the implementation and customising it to your needs.** The original documentation is below.
## Usage
To implement this wrapper there are two key APIs which are needed: [`WebView`](../api/web/com.google.accompanist.web/-web-view.html), which is provides the layout, and [`rememberWebViewState(url)`](../api/web/com.google.accompanist.web/remember-web-view-state.html) which provides some remembered state including the URL to display.
The basic usage is as follows:
```kotlin
val state = rememberWebViewState("https://example.com")
WebView(
state
)
```
This will display a WebView in your Compose layout that shows the URL provided.
There is a larger sample in the sample app which can be found [here](https://github.com/google/accompanist/blob/main/sample/src/main/java/com/google/accompanist/sample/webview/BasicWebViewSample.kt). This sample also shows how to show a loading state.
### WebView settings including JavaScript
By default, JavaScript is disabled in the WebView. To enable it or any other settings you can use the `onCreated` callback.
```kotlin
WebView(
state = webViewState,
onCreated = { it.settings.javaScriptEnabled = true }
)
```
### Capturing back presses
By default the WebView will capture back presses/swipes when relevant and navigate the WebView back. This can be disabled via the parameter on
the Composable.
```kotlin
WebView(
...
captureBackPresses = false
)
```
### Using a subclass of WebView
If you want to use a subclass of `WebView`, or simply require more control over its instantiation, you can provide a factory.
```kotlin
WebView(
...
factory = { context -> CustomWebView(context) }
)
```
## Download
[](https://search.maven.org/search?q=g:com.google.accompanist)
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-webview:"
}
```
================================================
FILE: drawablepainter/README.md
================================================
# Accompanist Drawable Painter
[](https://search.maven.org/search?q=g:com.google.accompanist)
For more information, visit the documentation: https://google.github.io/accompanist/drawablepainter
## Download
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-drawablepainter:"
}
```
================================================
FILE: drawablepainter/api/current.api
================================================
// Signature format: 4.0
package com.google.accompanist.drawablepainter {
public final class DrawablePainter extends androidx.compose.ui.graphics.painter.Painter implements androidx.compose.runtime.RememberObserver {
ctor public DrawablePainter(android.graphics.drawable.Drawable drawable);
method public android.graphics.drawable.Drawable getDrawable();
method public long getIntrinsicSize();
method public void onAbandoned();
method protected void onDraw(androidx.compose.ui.graphics.drawscope.DrawScope);
method public void onForgotten();
method public void onRemembered();
property public final android.graphics.drawable.Drawable drawable;
property public long intrinsicSize;
}
public final class DrawablePainterKt {
method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter rememberDrawablePainter(android.graphics.drawable.Drawable? drawable);
}
}
================================================
FILE: drawablepainter/build.gradle.kts
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
plugins {
alias(libs.plugins.accompanist.android.library)
alias(libs.plugins.accompanist.android.library.compose)
alias(libs.plugins.accompanist.android.library.published)
}
android {
namespace = "com.google.accompanist.drawablepainter"
}
dependencies {
implementation(libs.compose.ui.ui)
implementation(libs.kotlin.coroutines.android)
}
================================================
FILE: drawablepainter/gradle.properties
================================================
POM_ARTIFACT_ID=accompanist-drawablepainter
POM_NAME=Accompanist Drawable Painter library
POM_PACKAGING=aar
================================================
FILE: drawablepainter/src/main/AndroidManifest.xml
================================================
================================================
FILE: drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.drawablepainter
import android.graphics.drawable.Animatable
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asAndroidColorFilter
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.withSave
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.roundToInt
private val MAIN_HANDLER by lazy(LazyThreadSafetyMode.NONE) {
Handler(Looper.getMainLooper())
}
/**
* A [Painter] which draws an Android [Drawable] and supports [Animatable] drawables. Instances
* should be remembered to be able to start and stop [Animatable] animations.
*
* Instances are usually retrieved from [rememberDrawablePainter].
*/
public class DrawablePainter(
public val drawable: Drawable
) : Painter(), RememberObserver {
private var drawInvalidateTick by mutableStateOf(0)
private var drawableIntrinsicSize by mutableStateOf(drawable.intrinsicSize)
private val callback: Drawable.Callback by lazy {
object : Drawable.Callback {
override fun invalidateDrawable(d: Drawable) {
// Update the tick so that we get re-drawn
drawInvalidateTick++
// Update our intrinsic size too
drawableIntrinsicSize = drawable.intrinsicSize
}
override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) {
MAIN_HANDLER.postAtTime(what, time)
}
override fun unscheduleDrawable(d: Drawable, what: Runnable) {
MAIN_HANDLER.removeCallbacks(what)
}
}
}
init {
if (drawable.intrinsicWidth >= 0 && drawable.intrinsicHeight >= 0) {
// Update the drawable's bounds to match the intrinsic size
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
}
}
override fun onRemembered() {
drawable.callback = callback
drawable.setVisible(true, true)
if (drawable is Animatable) drawable.start()
}
override fun onAbandoned(): Unit = onForgotten()
override fun onForgotten() {
if (drawable is Animatable) drawable.stop()
drawable.setVisible(false, false)
drawable.callback = null
}
override fun applyAlpha(alpha: Float): Boolean {
drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255)
return true
}
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
drawable.colorFilter = colorFilter?.asAndroidColorFilter()
return true
}
override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean {
if (Build.VERSION.SDK_INT >= 23) {
return drawable.setLayoutDirection(
when (layoutDirection) {
LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR
LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL
}
)
}
return false
}
override val intrinsicSize: Size get() = drawableIntrinsicSize
override fun DrawScope.onDraw() {
drawIntoCanvas { canvas ->
// Reading this ensures that we invalidate when invalidateDrawable() is called
drawInvalidateTick
canvas.withSave {
// AnimatedImageDrawable is not respecting the bounds below Android 12, so this is
// a workaround to make the render size correct in this specific case
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
drawable is AnimatedImageDrawable
) {
canvas.scale(
size.width / intrinsicSize.width,
size.height / intrinsicSize.height
)
} else {
// Update the Drawable's bounds
drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
}
drawable.draw(canvas.nativeCanvas)
}
}
}
}
/**
* Remembers [Drawable] wrapped up as a [Painter]. This function attempts to un-wrap the
* drawable contents and use Compose primitives where possible.
*
* If the provided [drawable] is `null`, an empty no-op painter is returned.
*
* This function tries to dispatch lifecycle events to [drawable] as much as possible from
* within Compose.
*
* @sample com.google.accompanist.sample.drawablepainter.BasicSample
*/
@Composable
public fun rememberDrawablePainter(drawable: Drawable?): Painter = remember(drawable) {
when (drawable) {
null -> EmptyPainter
is ColorDrawable -> ColorPainter(Color(drawable.color))
// Since the DrawablePainter will be remembered and it implements RememberObserver, it
// will receive the necessary events
else -> DrawablePainter(drawable.mutate())
}
}
private val Drawable.intrinsicSize: Size
get() = when {
// Only return a finite size if the drawable has an intrinsic size
intrinsicWidth >= 0 && intrinsicHeight >= 0 -> {
Size(width = intrinsicWidth.toFloat(), height = intrinsicHeight.toFloat())
}
else -> Size.Unspecified
}
internal object EmptyPainter : Painter() {
override val intrinsicSize: Size get() = Size.Unspecified
override fun DrawScope.onDraw() {}
}
================================================
FILE: generate_docs.sh
================================================
#!/bin/bash
# Copyright 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Fail on any error
set -ex
DOCS_ROOT=docs-gen
[ -d $DOCS_ROOT ] && rm -r $DOCS_ROOT
mkdir $DOCS_ROOT
# Clear out the old API docs
[ -d docs/api ] && rm -r docs/api
# Build the docs with dokka
./gradlew dokkaHtmlMultiModule --stacktrace
# Create a copy of our docs at our $DOCS_ROOT
cp -a docs/* $DOCS_ROOT
cp README.md $DOCS_ROOT/index.md
cp CONTRIBUTING.md $DOCS_ROOT/contributing.md
sed -i.bak 's/CONTRIBUTING.md/contributing/' $DOCS_ROOT/index.md
sed -i.bak 's/README.md//' $DOCS_ROOT/index.md
sed -i.bak 's/docs\/header.png/header.png/' $DOCS_ROOT/index.md
# Convert docs/xxx.md links to just xxx/
sed -i.bak 's/docs\/\([a-zA-Z-]*\).md/\1/' $DOCS_ROOT/index.md
# Finally delete all of the backup files
find . -name '*.bak' -delete
================================================
FILE: gradle/libs.versions.toml
================================================
[versions]
compose = "1.7.0"
composeMaterial3 = "1.0.1"
composesnapshot = "-" # a single character = no snapshot
desugar_jdk_libs = "2.1.3"
dokka = "1.8.10"
# gradlePlugin and lint need to be updated together
androidTools = "31.7.2"
gradlePlugin = "8.7.3"
lintMinCompose = "30.0.0"
ktlint = "0.45.2"
kotlin = "2.0.20"
coroutines = "1.6.4"
coil = "1.3.2"
androidlint = "25.3.0"
androidxtest = "1.6.1"
androidxnavigation = "2.7.7"
androidxWindow = "1.0.0"
metalava = "0.3.5"
vanniktechPublish = "0.30.0"
[libraries]
compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "compose" }
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
compose-foundation-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "compose" }
compose-material-material = { module = "androidx.compose.material:material", version.ref = "compose" }
compose-material-iconsext = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
compose-material3-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial3" }
compose-animation-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" }
compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
apacheAnt = { module = "org.apache.ant:ant", version = "1.10.11" }
android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "gradlePlugin" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
gradleMavenPublishPlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "vanniktechPublish" }
metalavaGradle = { module = "me.tylerbwong.gradle.metalava:plugin", version.ref = "metalava" }
shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version = "8.3.3" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-stdlibJdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-metadataJvm = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" }
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
androidx-appcompat = "androidx.appcompat:appcompat:1.4.2"
androidx-core = "androidx.core:core-ktx:1.8.0"
androidx-activity-compose = "androidx.activity:activity-compose:1.9.0"
androidx-fragment = "androidx.fragment:fragment-ktx:1.8.1"
androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
androidx-lifecycle-viewmodel-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
androidx-window = { module = "androidx.window:window", version.ref = "androidxWindow" }
androidx-window-testing = { module = "androidx.window:window-testing", version.ref = "androidxWindow" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxnavigation" }
androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "androidxnavigation" }
mdc = "com.google.android.material:material:1.8.0"
androidx-test-core = "androidx.test:core-ktx:1.6.1"
androidx-test-runner = "androidx.test:runner:1.6.1"
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxtest" }
androidx-test-orchestrator = "androidx.test:orchestrator:1.5.0"
androidx-test-uiAutomator = "androidx.test.uiautomator:uiautomator:2.3.0"
junit = "junit:junit:4.13.2"
truth = "com.google.truth:truth:1.1.3"
robolectric = "org.robolectric:robolectric:4.12.1"
affectedmoduledetector = "com.dropbox.affectedmoduledetector:affectedmoduledetector:0.1.2"
android-tools-common = { module = "com.android.tools:common", version.ref = "androidTools" }
android-tools-lint-lint = { module = "com.android.tools.lint:lint", version.ref = "lintMinCompose" }
android-tools-lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lintMinCompose" }
android-tools-lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "lintMinCompose" }
[plugins]
android-application = { id = "com.android.application", version.ref = "gradlePlugin" }
android-kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "gradlePlugin" }
android-lint = { id = "com.android.lint", version.ref = "androidlint"}
jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
gradle-metalava = { id = "me.tylerbwong.gradle.metalava", version.ref = "metalava" }
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechPublish" }
compose-plugin = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
#build-logic plugins
accompanist-android-library = { id = "accompanist.android.library" }
accompanist-android-library-compose = { id = "accompanist.android.library.compose" }
accompanist-android-library-published = { id = "accompanist.android.library.published" }
accompanist-android-lint = { id = "accompanist.android.lint" }
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Wed Jul 10 11:49:25 AEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
#
# Copyright 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Turn on parallel compilation, caching and on-demand configuration
org.gradle.configureondemand=true
org.gradle.caching=true
org.gradle.parallel=true
# Declare we support AndroidX
android.useAndroidX=true
# Increase memory
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError
# Required to publish to Nexus (see https://github.com/gradle/gradle/issues/11308)
systemProp.org.gradle.internal.publish.checksums.insecure=true
# Increase timeout when pushing to Sonatype (otherwise we get timeouts)
systemProp.org.gradle.internal.http.socketTimeout=120000
GROUP=com.google.accompanist
# !! No longer need to update this manually when using a Compose SNAPSHOT
VERSION_NAME=0.37.4-SNAPSHOT
POM_DESCRIPTION=Utilities for Jetpack Compose
POM_URL=https://github.com/google/accompanist/
POM_SCM_URL=https://github.com/google/accompanist/
POM_SCM_CONNECTION=scm:git:git://github.com/google/accompanist.git
POM_SCM_DEV_CONNECTION=scm:git:git://github.com/google/accompanist.git
POM_LICENCE_NAME=The Apache Software License, Version 2.0
POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=google
POM_DEVELOPER_NAME=Google
SONATYPE_HOST=DEFAULT
RELEASE_SIGNING_ENABLED=true
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: internal-testutils/build.gradle.kts
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
plugins {
alias(libs.plugins.accompanist.android.library)
alias(libs.plugins.accompanist.android.library.compose)
}
android {
namespace = "com.google.accompanist.internal.test"
}
dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlin.coroutines.android)
implementation(libs.compose.foundation.foundation)
api(libs.compose.ui.test.junit4)
api(libs.androidx.test.core)
implementation(libs.truth)
}
================================================
FILE: internal-testutils/src/main/AndroidManifest.xml
================================================
================================================
FILE: internal-testutils/src/main/java/com/google/accompanist/internal/test/ActivityScenario.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.internal.test
import android.app.Activity
import androidx.test.core.app.ActivityScenario
public fun ActivityScenario .withActivity(
action: (A) -> T
): T {
lateinit var result: T
onActivity { result = action(it) }
return result
}
================================================
FILE: internal-testutils/src/main/java/com/google/accompanist/internal/test/Assertions.kt
================================================
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.internal.test
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertHeightIsAtLeast
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.unit.Dp
import com.google.common.truth.Truth.assertThat
/**
* Assert that all of the pixels in this image as of the [expected] color.
*/
public fun ImageBitmap.assertPixels(expected: Color, tolerance: Float = 0.001f) {
toPixelMap().buffer.forEach { pixel ->
val color = Color(pixel)
assertThat(color.red).isWithin(tolerance).of(expected.red)
assertThat(color.green).isWithin(tolerance).of(expected.green)
assertThat(color.blue).isWithin(tolerance).of(expected.blue)
assertThat(color.alpha).isWithin(tolerance).of(expected.alpha)
}
}
/**
* Run the [SemanticsNodeInteraction] provided by [block] repeatedly until either
* the assertion succeeds, or the execution runs past [timeoutMillis].
*/
public fun SemanticsNodeInteraction.assertWithTimeout(
timeoutMillis: Long,
block: SemanticsNodeInteraction.() -> SemanticsNodeInteraction,
): SemanticsNodeInteraction {
val startTime = System.nanoTime()
while (System.nanoTime() - startTime <= timeoutMillis * 1_000_000) {
try {
return block()
} catch (error: AssertionError) {
// If the assertion failed, sleep for 10ms before the next loop iteration
Thread.sleep(10)
}
}
// If we reach here, each assertion has failed and we've reached the time out.
// Run block one last time...
return block()
}
public val SemanticsNodeInteraction.exists: Boolean
get() = try {
assertExists()
true
} catch (t: Throwable) {
false
}
public val SemanticsNodeInteraction.isLaidOut: Boolean
get() = try {
assertWidthIsAtLeast(Dp.Hairline).assertHeightIsAtLeast(Dp.Hairline)
true
} catch (t: Throwable) {
false
}
================================================
FILE: internal-testutils/src/main/java/com/google/accompanist/internal/test/IgnoreOnRobolectric.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.internal.test
/**
* Marker interface to use as a category for filtering out tests when running on Robolectric.
*/
public interface IgnoreOnRobolectric
================================================
FILE: internal-testutils/src/main/java/com/google/accompanist/internal/test/TestUtils.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("NOTHING_TO_INLINE")
package com.google.accompanist.internal.test
public inline fun parameterizedParams(): List> = emptyList()
public inline fun List>.combineWithParameters(
vararg values: T
): List> {
if (isEmpty()) {
return values.map { arrayOf(it) }
}
return fold(emptyList()) { acc, args ->
val result = acc.toMutableList()
values.forEach { v ->
result += ArrayList().apply {
addAll(args)
add(v)
}.toTypedArray()
}
result.toList()
}
}
================================================
FILE: internal-testutils/src/main/java/com/google/accompanist/internal/test/WaitUntil.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.internal.test
import android.os.Looper
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.TimeoutException
public fun waitUntil(timeoutMillis: Long = 2_000, condition: () -> Boolean) {
if (Looper.getMainLooper() == Looper.myLooper()) {
assertThat(condition()).isTrue()
}
val startTime = System.nanoTime()
while (!condition()) {
// Let Android run measure, draw and in general any other async operations.
Thread.sleep(10)
if (System.nanoTime() - startTime > timeoutMillis * 1_000_000) {
throw TimeoutException("Condition still not satisfied after $timeoutMillis ms")
}
}
}
================================================
FILE: internal-testutils/src/main/res/values/themes.xml
================================================
================================================
FILE: mkdocs.yml
================================================
# Project information
site_name: 'Accompanist'
site_description: 'A group of libraries to help write Jetpack Compose apps.'
site_author: 'Google'
site_url: 'https://google.github.io/accompanist/'
edit_uri: 'tree/main/docs/'
remote_branch: gh-pages
docs_dir: docs-gen
# Repository
repo_name: 'Accompanist'
repo_url: 'https://github.com/google/accompanist'
# Navigation
nav:
- 'Overview': index.md
- 'System UI Controller':
- 'Guide': systemuicontroller.md
- 'API': api/systemuicontroller/
- 'Drawable Painter':
- 'Guide': drawablepainter.md
- 'API': api/drawablepainter/
- 'Permissions':
- 'Guide': permissions.md
- 'API': api/permissions/
- 'Navigation Animation':
- 'Guide': navigation-animation.md
- 'API': api/navigation-animation/
- 'Navigation Material':
- 'Guide': navigation-material.md
- 'API': api/navigation-material/
- 'Adaptive':
- 'Guide': adaptive.md
- 'API': api/adaptive
- 'Snapshots': using-snapshot-version.md
- 'Contributing': contributing.md
- 'Maintainers':
- 'Update guide': updating.md
# Configuration
theme:
name: 'material'
language: 'en'
icon:
logo: material/music-clef-treble
palette:
primary: 'black'
accent: 'deep orange'
font:
text: 'Roboto'
code: 'JetBrains Mono'
# Extensions
markdown_extensions:
- admonition
- attr_list
- codehilite:
guess_lang: false
- footnotes
- toc:
permalink: true
- pymdownx.betterem
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.details
================================================
FILE: permissions/README.md
================================================
# Permissions for Jetpack Compose
[](https://search.maven.org/search?q=g:com.google.accompanist)
For more information, visit the documentation: https://google.github.io/accompanist/permissions
## Download
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-permissions:"
}
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-permissions/
================================================
FILE: permissions/api/current.api
================================================
// Signature format: 4.0
package com.google.accompanist.permissions {
@kotlin.RequiresOptIn(message="Accompanist Permissions is experimental. The API may be changed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalPermissionsApi {
}
@androidx.compose.runtime.Stable @com.google.accompanist.permissions.ExperimentalPermissionsApi public interface MultiplePermissionsState {
method public boolean getAllPermissionsGranted();
method public java.util.List getPermissions();
method public java.util.List getRevokedPermissions();
method public boolean getShouldShowRationale();
method public void launchMultiplePermissionRequest();
property public abstract boolean allPermissionsGranted;
property public abstract java.util.List permissions;
property public abstract java.util.List revokedPermissions;
property public abstract boolean shouldShowRationale;
}
public final class MultiplePermissionsStateKt {
method @androidx.compose.runtime.Composable @com.google.accompanist.permissions.ExperimentalPermissionsApi public static com.google.accompanist.permissions.MultiplePermissionsState rememberMultiplePermissionsState(java.util.List permissions, optional kotlin.jvm.functions.Function1 super java.util.Map,kotlin.Unit> onPermissionsResult);
method @androidx.compose.runtime.Composable @com.google.accompanist.permissions.ExperimentalPermissionsApi public static com.google.accompanist.permissions.MultiplePermissionsState rememberMultiplePermissionsState(java.util.List permissions, optional kotlin.jvm.functions.Function1 super java.util.Map,kotlin.Unit> onPermissionsResult, optional java.util.Map previewPermissionStatuses);
}
@androidx.compose.runtime.Stable @com.google.accompanist.permissions.ExperimentalPermissionsApi public interface PermissionState {
method public String getPermission();
method public com.google.accompanist.permissions.PermissionStatus getStatus();
method public void launchPermissionRequest();
property public abstract String permission;
property public abstract com.google.accompanist.permissions.PermissionStatus status;
}
public final class PermissionStateKt {
method @androidx.compose.runtime.Composable @com.google.accompanist.permissions.ExperimentalPermissionsApi public static com.google.accompanist.permissions.PermissionState rememberPermissionState(String permission, optional kotlin.jvm.functions.Function1 super java.lang.Boolean,kotlin.Unit> onPermissionResult);
method @androidx.compose.runtime.Composable @com.google.accompanist.permissions.ExperimentalPermissionsApi public static com.google.accompanist.permissions.PermissionState rememberPermissionState(String permission, optional kotlin.jvm.functions.Function1 super java.lang.Boolean,kotlin.Unit> onPermissionResult, optional com.google.accompanist.permissions.PermissionStatus previewPermissionStatus);
}
@androidx.compose.runtime.Stable @com.google.accompanist.permissions.ExperimentalPermissionsApi public sealed interface PermissionStatus {
}
public static final class PermissionStatus.Denied implements com.google.accompanist.permissions.PermissionStatus {
ctor public PermissionStatus.Denied(boolean shouldShowRationale);
method public boolean component1();
method public com.google.accompanist.permissions.PermissionStatus.Denied copy(boolean shouldShowRationale);
method public boolean getShouldShowRationale();
property public final boolean shouldShowRationale;
}
public static final class PermissionStatus.Granted implements com.google.accompanist.permissions.PermissionStatus {
field public static final com.google.accompanist.permissions.PermissionStatus.Granted INSTANCE;
}
public final class PermissionsUtilKt {
method public static boolean getShouldShowRationale(com.google.accompanist.permissions.PermissionStatus);
method public static boolean isGranted(com.google.accompanist.permissions.PermissionStatus);
}
}
================================================
FILE: permissions/build.gradle.kts
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
plugins {
alias(libs.plugins.accompanist.android.library)
alias(libs.plugins.accompanist.android.library.compose)
alias(libs.plugins.accompanist.android.library.published)
}
android {
namespace = "com.google.accompanist.permissions"
defaultConfig {
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
}
dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.compose.foundation.foundation)
implementation(libs.kotlin.coroutines.android)
lintChecks(project(":permissions-lint"))
lintPublish(project(":permissions-lint"))
// ======================
// Test dependencies
// ======================
androidTestUtil(libs.androidx.test.orchestrator)
androidTestImplementation(project(":internal-testutils"))
androidTestImplementation(libs.androidx.activity.compose)
androidTestImplementation(libs.compose.material.material)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.compose.ui.test.junit4)
androidTestImplementation(libs.compose.ui.test.manifest)
androidTestImplementation(libs.compose.foundation.foundation)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.uiAutomator)
}
================================================
FILE: permissions/gradle.properties
================================================
POM_ARTIFACT_ID=accompanist-permissions
POM_NAME=Accompanist Permissions
POM_PACKAGING=aar
================================================
FILE: permissions/src/androidTest/AndroidManifest.xml
================================================
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/FakeTests.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import androidx.test.filters.SdkSuppress
import org.junit.Test
/**
* Fake tests to avoid the "No tests found error" when running in Build.VERSION.SDK_INT < 23
*/
class FakeTests {
@SdkSuppress(maxSdkVersion = 22)
@Test
fun fakeTestToAvoidNoTestsFoundErrorInAPI22AndBelow() = Unit
// More Fake tests to help with sharding: https://github.com/android/android-test/issues/973
@Test
fun fake1() = Unit
@Test
fun fake2() = Unit
@Test
fun fake3() = Unit
@Test
fun fake4() = Unit
@Test
fun fake5() = Unit
@Test
fun fake6() = Unit
@Test
fun fake7() = Unit
@Test
fun fake8() = Unit
@Test
fun fake9() = Unit
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/MultipleAndSinglePermissionsTest.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import android.content.Intent
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.filters.FlakyTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.accompanist.permissions.test.PermissionsTestActivity
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalPermissionsApi::class)
@FlakyTest(detail = "https://github.com/google/accompanist/issues/490")
@SdkSuppress(minSdkVersion = 23)
class MultipleAndSinglePermissionsTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
private val instrumentation by lazy {
InstrumentationRegistry.getInstrumentation()
}
private val uiDevice by lazy {
UiDevice.getInstance(instrumentation)
}
@Test
fun singlePermission_granted() {
composeTestRule.setContent {
ComposableUnderTest(listOf(android.Manifest.permission.CAMERA))
}
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun singlePermission_deniedAndGrantedInSecondActivity() {
composeTestRule.setContent {
ComposableUnderTest(listOf(android.Manifest.permission.CAMERA))
}
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
denyPermissionInDialog()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
uiDevice.pressBack()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun singlePermission_deniedAndGrantedInFirstActivity() {
composeTestRule.setContent {
ComposableUnderTest(listOf(android.Manifest.permission.CAMERA))
}
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
denyPermissionInDialog()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
uiDevice.pressBack()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun singlePermission_deniedAndGrantedInFirstActivity_singlePermission() {
composeTestRule.setContent {
ComposableUnderTest(
listOf(android.Manifest.permission.CAMERA),
requestSinglePermission = true
)
}
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
denyPermissionInDialog()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
uiDevice.pressBack()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun multiplePermissions_granted() {
composeTestRule.setContent {
ComposableUnderTest(
listOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.CAMERA
)
)
}
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // Grant first permission
grantPermissionInDialog() // Grant second permission
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun multiplePermissions_denied() {
composeTestRule.setContent {
ComposableUnderTest(
listOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.CAMERA
)
)
}
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
denyPermissionInDialog() // Deny first permission
denyPermissionInDialog() // Deny second permission
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Navigate").performClick()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("PermissionsTestActivity").assertIsDisplayed()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // Grant the permission
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
uiDevice.pressBack()
instrumentation.waitForIdleSync()
composeTestRule.onNodeWithText("MultipleAndSinglePermissionsTest").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // only one permission to grant now
if (Build.VERSION.SDK_INT == 23) { // API 23 shows all permissions again
grantPermissionInDialog()
}
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Composable
private fun ComposableUnderTest(
permissions: List,
requestSinglePermission: Boolean = false
) {
val state = rememberMultiplePermissionsState(permissions)
Column {
Text("MultipleAndSinglePermissionsTest")
Spacer(Modifier.height(16.dp))
if (state.allPermissionsGranted) {
Text("Granted")
} else {
Column {
val textToShow = if (state.shouldShowRationale) {
"ShowRationale"
} else {
"No permission"
}
Text(textToShow)
Button(
onClick = {
if (requestSinglePermission && state.revokedPermissions.size == 1) {
state.revokedPermissions[0].launchPermissionRequest()
} else {
state.launchMultiplePermissionRequest()
}
}
) {
Text("Request")
}
}
}
Spacer(Modifier.height(16.dp))
Button(
onClick = {
composeTestRule.activity.startActivity(
Intent(composeTestRule.activity, PermissionsTestActivity::class.java)
)
}
) {
Text("Navigate")
}
}
}
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/MultiplePermissionsStateTest.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.filters.SdkSuppress
import androidx.test.rule.GrantPermissionRule
import com.google.accompanist.permissions.test.EmptyPermissionsTestActivity
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
/**
* Simple tests that prove the data comes from the right place
*/
@OptIn(ExperimentalPermissionsApi::class)
@SdkSuppress(minSdkVersion = 23)
class MultiplePermissionsStateTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
"android.permission.CAMERA",
"android.permission.ACCESS_FINE_LOCATION"
)
@Test
fun permissionState_hasPermission() {
composeTestRule.setContent {
val state = rememberMultiplePermissionsState(
listOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.CAMERA
)
)
assertThat(state.allPermissionsGranted).isTrue()
assertThat(state.shouldShowRationale).isFalse()
}
}
@Test
fun permissionTest_shouldShowRationale() {
composeTestRule.activity.shouldShowRequestPermissionRationale = mapOf(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE to true
)
composeTestRule.setContent {
val state = rememberMultiplePermissionsState(
listOf(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.CAMERA
)
)
assertThat(state.allPermissionsGranted).isFalse()
assertThat(state.shouldShowRationale).isTrue()
assertThat(state.permissions).hasSize(3)
assertThat(state.revokedPermissions).hasSize(1)
assertThat(state.revokedPermissions[0].permission)
.isEqualTo("android.permission.WRITE_EXTERNAL_STORAGE")
}
}
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/PermissionStateTest.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.filters.SdkSuppress
import androidx.test.rule.GrantPermissionRule
import com.google.accompanist.permissions.test.EmptyPermissionsTestActivity
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
/**
* Simple tests that prove the data comes from the right place
*/
@OptIn(ExperimentalPermissionsApi::class)
@SdkSuppress(minSdkVersion = 23)
class PermissionStateTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
@get:Rule
val permissionRule: GrantPermissionRule =
GrantPermissionRule.grant("android.permission.CAMERA")
@Test
fun permissionState_hasPermission() {
composeTestRule.setContent {
val state = rememberPermissionState(android.Manifest.permission.CAMERA)
assertThat(state.status.isGranted).isTrue()
assertThat(state.status.shouldShowRationale).isFalse()
}
}
@Test
fun permissionTest_shouldShowRationale() {
val permission = android.Manifest.permission.ACCESS_FINE_LOCATION
composeTestRule.activity.shouldShowRequestPermissionRationale = mapOf(
permission to true
)
composeTestRule.setContent {
val state = rememberPermissionState(permission)
assertThat(state.status.isGranted).isFalse()
assertThat(state.status.shouldShowRationale).isTrue()
}
}
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/RequestMultiplePermissionsTest.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.SdkSuppress
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalPermissionsApi::class)
@SdkSuppress(minSdkVersion = 27) // Flaky on 26
class RequestMultiplePermissionsTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
@Before
fun setup() {
composeTestRule.setContent { ComposableUnderTest() }
}
@Test
fun permissionTest_grantPermissions() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // Grant first permission
grantPermissionInDialog() // Grant second permission
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun permissionTest_denyOnePermission() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // Grant first permission
denyPermissionInDialog() // Deny second permission
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // Grant second permission
if (Build.VERSION.SDK_INT == 23) { // API 23 shows all permissions again
grantPermissionInDialog()
}
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun permissionTest_doNotAskAgainPermission() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // Grant first permission
denyPermissionInDialog() // Deny second permission
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
if (Build.VERSION.SDK_INT == 23) { // API 23 shows all permissions again
grantPermissionInDialog()
}
doNotAskAgainPermissionInDialog() // Do not ask again second permission
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
}
@Test
fun permissionTest_grantInTheBackground() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog() // Grant first permission
denyPermissionInDialog() // Deny second permission
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
if (Build.VERSION.SDK_INT == 23) { // API 23 shows all permissions again
grantPermissionInDialog()
}
doNotAskAgainPermissionInDialog() // Do not ask again second permission
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
// This simulates the user going to the Settings screen and granting both permissions.
// This is cheating, I know, but the order in which the system request the permissions
// is unpredictable. Therefore, we need to grant both to make this test deterministic.
grantPermissionProgrammatically("android.permission.CAMERA")
grantPermissionProgrammatically("android.permission.ACCESS_FINE_LOCATION")
simulateAppComingFromTheBackground(composeTestRule)
composeTestRule.activityRule.scenario.onActivity {
it.setContent { ComposableUnderTest() }
}
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Composable
private fun ComposableUnderTest() {
val state = rememberMultiplePermissionsState(
listOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.CAMERA
)
)
if (state.allPermissionsGranted) {
Text("Granted")
} else {
Column {
val textToShow = if (state.shouldShowRationale) {
"ShowRationale"
} else {
"No permission"
}
Text(textToShow)
Button(onClick = { state.launchMultiplePermissionRequest() }) {
Text("Request")
}
}
}
}
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/RequestPermissionTest.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.FlakyTest
import androidx.test.filters.SdkSuppress
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalPermissionsApi::class)
@SdkSuppress(minSdkVersion = 23)
class RequestPermissionTest {
@get:Rule
val composeTestRule = createAndroidComposeRule()
@Before
fun setup() {
composeTestRule.setContent { ComposableUnderTest() }
}
@Test
fun permissionTest_grantPermission() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun permissionTest_denyPermission() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
denyPermissionInDialog()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
grantPermissionInDialog()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Test
fun permissionTest_doNotAskAgainPermission() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
denyPermissionInDialog()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
doNotAskAgainPermissionInDialog()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
}
@SdkSuppress(minSdkVersion = 29) // Flaky below
@FlakyTest
@Test
fun permissionTest_grantInTheBackground() {
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
denyPermissionInDialog()
composeTestRule.onNodeWithText("ShowRationale").assertIsDisplayed()
composeTestRule.onNodeWithText("Request").performClick()
doNotAskAgainPermissionInDialog()
composeTestRule.onNodeWithText("No permission").assertIsDisplayed()
// This simulates the user going to the Settings screen and granting the permission
grantPermissionProgrammatically(android.Manifest.permission.CAMERA)
simulateAppComingFromTheBackground(composeTestRule)
composeTestRule.activityRule.scenario.onActivity {
it.setContent { ComposableUnderTest() }
}
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("Granted").assertIsDisplayed()
}
@Composable
private fun ComposableUnderTest() {
val state = rememberPermissionState(android.Manifest.permission.CAMERA)
when (state.status) {
PermissionStatus.Granted -> {
Text("Granted")
}
is PermissionStatus.Denied -> {
Column {
val textToShow = if (state.status.shouldShowRationale) {
"ShowRationale"
} else {
"No permission"
}
Text(textToShow)
Button(onClick = { state.launchPermissionRequest() }) {
Text("Request")
}
}
}
}
}
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/TestUtils.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import android.app.Instrumentation
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.lifecycle.Lifecycle
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import java.io.ByteArrayOutputStream
internal fun simulateAppComingFromTheBackground(
composeTestRule: AndroidComposeTestRule, T>
) {
// Make Activity go through ON_START, and ON_RESUME
composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.STARTED)
composeTestRule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
}
internal fun grantPermissionProgrammatically(
permission: String,
instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
) {
if (Build.VERSION.SDK_INT < 28) {
val fileDescriptor = instrumentation.uiAutomation.executeShellCommand(
"pm grant ${instrumentation.targetContext.packageName} $permission"
)
fileDescriptor.checkError()
fileDescriptor.close()
} else {
instrumentation.uiAutomation.grantRuntimePermission(
instrumentation.targetContext.packageName, permission
)
}
}
internal fun grantPermissionInDialog(
instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
) {
val uiDevice = UiDevice.getInstance(instrumentation)
val sdkVersion = Build.VERSION.SDK_INT
val button = uiDevice.findPermissionButton(
when (sdkVersion) {
in 24..28 -> "ALLOW"
29 -> "Allow"
else -> "While using the app"
}
)
button.clickForPermission(instrumentation)
}
internal fun denyPermissionInDialog(
instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
) {
val text = when (Build.VERSION.SDK_INT) {
in 24..28 -> "DENY"
in 29..30 -> "Deny"
else -> "t allow" // Different sdks and devices seem to have either ' or ’
}
val permissionButton = UiDevice.getInstance(instrumentation).findPermissionButton(text)
permissionButton.clickForPermission(instrumentation)
}
internal fun doNotAskAgainPermissionInDialog(
instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
) {
val uiDevice = UiDevice.getInstance(instrumentation)
when {
Build.VERSION.SDK_INT >= 30 -> {
denyPermissionInDialog(instrumentation)
}
Build.VERSION.SDK_INT > 28 -> {
uiDevice
.findPermissionButton("Deny & don’t ask again")
.clickForPermission(instrumentation)
}
Build.VERSION.SDK_INT == 23 -> {
uiDevice.findPermissionButton("Never ask again")
.clickForPermission(instrumentation)
denyPermissionInDialog(instrumentation)
}
else -> {
uiDevice.findPermissionButton(
"Don't ask again"
).clickForPermission(instrumentation)
denyPermissionInDialog(instrumentation)
}
}
}
private fun UiDevice.findPermissionButton(
text: String
): UiObject2 {
val selector = By
.textContains(text)
.clickable(true)
val found = wait(Until.hasObject(selector), 3000)
if (!found) {
val output = ByteArrayOutputStream()
dumpWindowHierarchy(output)
println(output.toByteArray().decodeToString())
error("Could not find button with text $text")
}
return findObject(selector)
}
private fun UiObject2.clickForPermission(
instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
): Boolean {
click()
// Make sure that the tests waits for this click to be processed
instrumentation.waitForIdleSync()
return true
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/test/EmptyPermissionsTestActivity.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions.test
import androidx.activity.ComponentActivity
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@OptIn(ExperimentalPermissionsApi::class)
class EmptyPermissionsTestActivity : ComponentActivity() {
var shouldShowRequestPermissionRationale: Map = emptyMap()
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
if (permission in shouldShowRequestPermissionRationale.keys) {
return shouldShowRequestPermissionRationale[permission]!!
}
return super.shouldShowRequestPermissionRationale(permission)
}
}
================================================
FILE: permissions/src/androidTest/java/com/google/accompanist/permissions/test/PermissionsTestActivity.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions.test
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.Text
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
@OptIn(ExperimentalPermissionsApi::class)
class PermissionsTestActivity : ComponentActivity() {
var shouldShowRequestPermissionRationale: Map = emptyMap()
override fun shouldShowRequestPermissionRationale(permission: String): Boolean {
if (permission in shouldShowRequestPermissionRationale.keys) {
return shouldShowRequestPermissionRationale[permission]!!
}
return super.shouldShowRequestPermissionRationale(permission)
}
/**
* Code used in `MultipleAndSinglePermissionsTest`
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
Text("PermissionsTestActivity")
val state = rememberPermissionState(android.Manifest.permission.CAMERA,)
when (state.status) {
PermissionStatus.Granted -> {
Text("Granted")
}
is PermissionStatus.Denied -> {
val textToShow = if (state.status.shouldShowRationale) {
"ShowRationale"
} else {
"No permission"
}
Text(textToShow)
Button(onClick = { state.launchPermissionRequest() }) {
Text("Request")
}
}
}
}
}
}
}
================================================
FILE: permissions/src/main/AndroidManifest.xml
================================================
================================================
FILE: permissions/src/main/java/com/google/accompanist/permissions/MultiplePermissionsState.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.util.fastMap
/**
* Creates a [MultiplePermissionsState] that is remembered across compositions.
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*
* @param permissions the permissions to control and observe.
* @param onPermissionsResult will be called with whether or not the user granted the permissions
* after [MultiplePermissionsState.launchMultiplePermissionRequest] is called.
*/
@ExperimentalPermissionsApi
@Composable
public fun rememberMultiplePermissionsState(
permissions: List,
onPermissionsResult: (Map) -> Unit = {}
): MultiplePermissionsState {
return rememberMultiplePermissionsState(permissions, onPermissionsResult, emptyMap())
}
/**
* Creates a [MultiplePermissionsState] that is remembered across compositions.
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*
* @param permissions the permissions to control and observe.
* @param onPermissionsResult will be called with whether or not the user granted the permissions
* after [MultiplePermissionsState.launchMultiplePermissionRequest] is called.
* @param previewPermissionStatuses provides a [PermissionStatus] for a given permission when running
* in a preview.
*/
@ExperimentalPermissionsApi
@Composable
public fun rememberMultiplePermissionsState(
permissions: List,
onPermissionsResult: (Map) -> Unit = {},
previewPermissionStatuses: Map = emptyMap()
): MultiplePermissionsState {
return when {
LocalInspectionMode.current ->
PreviewMultiplePermissionsState(permissions, previewPermissionStatuses)
else -> rememberMutableMultiplePermissionsState(permissions, onPermissionsResult)
}
}
/**
* A state object that can be hoisted to control and observe multiple [permissions] status changes.
*
* In most cases, this will be created via [rememberMultiplePermissionsState].
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*/
@ExperimentalPermissionsApi
@Stable
public interface MultiplePermissionsState {
/**
* List of all permissions to request.
*/
public val permissions: List
/**
* List of permissions revoked by the user.
*/
public val revokedPermissions: List
/**
* When `true`, the user has granted all [permissions].
*/
public val allPermissionsGranted: Boolean
/**
* When `true`, the user should be presented with a rationale.
*/
public val shouldShowRationale: Boolean
/**
* Request the [permissions] to the user.
*
* This should always be triggered from non-composable scope, for example, from a side-effect
* or a non-composable callback. Otherwise, this will result in an IllegalStateException.
*
* This triggers a system dialog that asks the user to grant or revoke the permission.
* Note that this dialog might not appear on the screen if the user doesn't want to be asked
* again or has denied the permission multiple times.
* This behavior varies depending on the Android level API.
*/
public fun launchMultiplePermissionRequest(): Unit
}
@OptIn(ExperimentalPermissionsApi::class)
@Immutable
private class PreviewMultiplePermissionsState(
permissions: List,
permissionStatuses: Map
) : MultiplePermissionsState {
override val permissions: List = permissions.fastMap { permission ->
PreviewPermissionState(
permission = permission,
status = permissionStatuses[permission] ?: PermissionStatus.Granted,
)
}
override val revokedPermissions: List = emptyList()
override val allPermissionsGranted: Boolean = false
override val shouldShowRationale: Boolean = false
override fun launchMultiplePermissionRequest() {}
}
================================================
FILE: permissions/src/main/java/com/google/accompanist/permissions/MutableMultiplePermissionsState.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
/**
* Creates a [MultiplePermissionsState] that is remembered across compositions.
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*
* @param permissions the permissions to control and observe.
* @param onPermissionsResult will be called with whether or not the user granted the permissions
* after [MultiplePermissionsState.launchMultiplePermissionRequest] is called.
*/
@ExperimentalPermissionsApi
@Composable
internal fun rememberMutableMultiplePermissionsState(
permissions: List,
onPermissionsResult: (Map) -> Unit = {}
): MultiplePermissionsState {
// Create mutable permissions that can be requested individually
val mutablePermissions = rememberMutablePermissionsState(permissions)
// Refresh permissions when the lifecycle is resumed.
PermissionsLifecycleCheckerEffect(mutablePermissions)
val multiplePermissionsState = remember(permissions) {
MutableMultiplePermissionsState(mutablePermissions)
}
// Remember RequestMultiplePermissions launcher and assign it to multiplePermissionsState
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissionsResult ->
multiplePermissionsState.updatePermissionsStatus(permissionsResult)
onPermissionsResult(permissionsResult)
}
DisposableEffect(multiplePermissionsState, launcher) {
multiplePermissionsState.launcher = launcher
onDispose {
multiplePermissionsState.launcher = null
}
}
return multiplePermissionsState
}
@ExperimentalPermissionsApi
@Composable
private fun rememberMutablePermissionsState(
permissions: List
): List {
// Create list of MutablePermissionState for each permission
val context = LocalContext.current
val activity = context.findActivity()
val mutablePermissions: List = remember(permissions) {
return@remember permissions.map { MutablePermissionState(it, context, activity) }
}
// Update each permission with its own launcher
for (permissionState in mutablePermissions) {
key(permissionState.permission) {
// Remember launcher and assign it to the permissionState
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) {
permissionState.refreshPermissionStatus()
}
DisposableEffect(launcher) {
permissionState.launcher = launcher
onDispose {
permissionState.launcher = null
}
}
}
}
return mutablePermissions
}
/**
* A state object that can be hoisted to control and observe multiple permission status changes.
*
* In most cases, this will be created via [rememberMutableMultiplePermissionsState].
*
* @param mutablePermissions list of mutable permissions to control and observe.
*/
@ExperimentalPermissionsApi
@Stable
internal class MutableMultiplePermissionsState(
private val mutablePermissions: List
) : MultiplePermissionsState {
override val permissions: List = mutablePermissions
override val revokedPermissions: List by derivedStateOf {
permissions.filter { it.status != PermissionStatus.Granted }
}
override val allPermissionsGranted: Boolean by derivedStateOf {
permissions.all { it.status.isGranted } || // Up to date when the lifecycle is resumed
revokedPermissions.isEmpty() // Up to date when the user launches the action
}
override val shouldShowRationale: Boolean by derivedStateOf {
permissions.any { it.status.shouldShowRationale } &&
permissions.none { !it.status.isGranted && !it.status.shouldShowRationale }
}
override fun launchMultiplePermissionRequest() {
launcher?.launch(
permissions.map { it.permission }.toTypedArray()
) ?: throw IllegalStateException("ActivityResultLauncher cannot be null")
}
internal var launcher: ActivityResultLauncher>? = null
internal fun updatePermissionsStatus(permissionsStatus: Map) {
// Update all permissions with the result
for (permission in permissionsStatus.keys) {
mutablePermissions.firstOrNull { it.permission == permission }?.apply {
permissionsStatus[permission]?.let {
this.refreshPermissionStatus()
}
}
}
}
}
================================================
FILE: permissions/src/main/java/com/google/accompanist/permissions/MutablePermissionState.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import android.app.Activity
import android.content.Context
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
/**
* Creates a [MutablePermissionState] that is remembered across compositions.
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*
* @param permission the permission to control and observe.
* @param onPermissionResult will be called with whether or not the user granted the permission
* after [PermissionState.launchPermissionRequest] is called.
*/
@ExperimentalPermissionsApi
@Composable
internal fun rememberMutablePermissionState(
permission: String,
onPermissionResult: (Boolean) -> Unit = {}
): MutablePermissionState {
val context = LocalContext.current
val permissionState = remember(permission) {
MutablePermissionState(permission, context, context.findActivity())
}
// Refresh the permission status when the lifecycle is resumed
PermissionLifecycleCheckerEffect(permissionState)
// Remember RequestPermission launcher and assign it to permissionState
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
permissionState.refreshPermissionStatus()
onPermissionResult(it)
}
DisposableEffect(permissionState, launcher) {
permissionState.launcher = launcher
onDispose {
permissionState.launcher = null
}
}
return permissionState
}
/**
* A mutable state object that can be used to control and observe permission status changes.
*
* In most cases, this will be created via [rememberMutablePermissionState].
*
* @param permission the permission to control and observe.
* @param context to check the status of the [permission].
* @param activity to check if the user should be presented with a rationale for [permission].
*/
@ExperimentalPermissionsApi
@Stable
internal class MutablePermissionState(
override val permission: String,
private val context: Context,
private val activity: Activity
) : PermissionState {
override var status: PermissionStatus by mutableStateOf(getPermissionStatus())
override fun launchPermissionRequest() {
launcher?.launch(
permission
) ?: throw IllegalStateException("ActivityResultLauncher cannot be null")
}
internal var launcher: ActivityResultLauncher? = null
internal fun refreshPermissionStatus() {
status = getPermissionStatus()
}
private fun getPermissionStatus(): PermissionStatus {
val hasPermission = context.checkPermission(permission)
return if (hasPermission) {
PermissionStatus.Granted
} else {
PermissionStatus.Denied(activity.shouldShowRationale(permission))
}
}
}
================================================
FILE: permissions/src/main/java/com/google/accompanist/permissions/PermissionState.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.platform.LocalInspectionMode
/**
* Creates a [PermissionState] that is remembered across compositions.
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*
* @param permission the permission to control and observe.
* @param onPermissionResult will be called with whether or not the user granted the permission
* after [PermissionState.launchPermissionRequest] is called.
*/
@ExperimentalPermissionsApi
@Composable
public fun rememberPermissionState(
permission: String,
onPermissionResult: (Boolean) -> Unit = {}
): PermissionState {
return rememberPermissionState(permission, onPermissionResult, PermissionStatus.Granted)
}
/**
* Creates a [PermissionState] that is remembered across compositions.
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*
* @param permission the permission to control and observe.
* @param onPermissionResult will be called with whether or not the user granted the permission
* after [PermissionState.launchPermissionRequest] is called.
* @param previewPermissionStatus provides a [PermissionStatus] when running in a preview.
*/
@ExperimentalPermissionsApi
@Composable
public fun rememberPermissionState(
permission: String,
onPermissionResult: (Boolean) -> Unit = {},
previewPermissionStatus: PermissionStatus = PermissionStatus.Granted
): PermissionState {
return when {
LocalInspectionMode.current -> PreviewPermissionState(permission, previewPermissionStatus)
else -> rememberMutablePermissionState(permission, onPermissionResult)
}
}
/**
* A state object that can be hoisted to control and observe [permission] status changes.
*
* In most cases, this will be created via [rememberPermissionState].
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*/
@ExperimentalPermissionsApi
@Stable
public interface PermissionState {
/**
* The permission to control and observe.
*/
public val permission: String
/**
* [permission]'s status
*/
public val status: PermissionStatus
/**
* Request the [permission] to the user.
*
* This should always be triggered from non-composable scope, for example, from a side-effect
* or a non-composable callback. Otherwise, this will result in an IllegalStateException.
*
* This triggers a system dialog that asks the user to grant or revoke the permission.
* Note that this dialog might not appear on the screen if the user doesn't want to be asked
* again or has denied the permission multiple times.
* This behavior varies depending on the Android level API.
*/
public fun launchPermissionRequest(): Unit
}
@OptIn(ExperimentalPermissionsApi::class)
@Immutable
internal class PreviewPermissionState(
override val permission: String,
override val status: PermissionStatus
) : PermissionState {
override fun launchPermissionRequest() {}
}
================================================
FILE: permissions/src/main/java/com/google/accompanist/permissions/PermissionsUtil.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@RequiresOptIn(message = "Accompanist Permissions is experimental. The API may be changed in the future.")
@Retention(AnnotationRetention.BINARY)
public annotation class ExperimentalPermissionsApi
/**
* Model of the status of a permission. It can be granted or denied.
* If denied, the user might need to be presented with a rationale.
*/
@ExperimentalPermissionsApi
@Stable
public sealed interface PermissionStatus {
public object Granted : PermissionStatus
public data class Denied(
val shouldShowRationale: Boolean
) : PermissionStatus
}
/**
* `true` if the permission is granted.
*/
@ExperimentalPermissionsApi
public val PermissionStatus.isGranted: Boolean
get() = this == PermissionStatus.Granted
/**
* `true` if a rationale should be presented to the user.
*/
@ExperimentalPermissionsApi
public val PermissionStatus.shouldShowRationale: Boolean
get() = when (this) {
PermissionStatus.Granted -> false
is PermissionStatus.Denied -> shouldShowRationale
}
/**
* Effect that updates the `hasPermission` state of a revoked [MutablePermissionState] permission
* when the lifecycle gets called with [lifecycleEvent].
*/
@ExperimentalPermissionsApi
@Composable
internal fun PermissionLifecycleCheckerEffect(
permissionState: MutablePermissionState,
lifecycleEvent: Lifecycle.Event = Lifecycle.Event.ON_RESUME
) {
// Check if the permission was granted when the lifecycle is resumed.
// The user might've gone to the Settings screen and granted the permission.
val permissionCheckerObserver = remember(permissionState) {
LifecycleEventObserver { _, event ->
if (event == lifecycleEvent) {
// If the permission is revoked, check again.
// We don't check if the permission was denied as that triggers a process restart.
if (permissionState.status != PermissionStatus.Granted) {
permissionState.refreshPermissionStatus()
}
}
}
}
val lifecycle = androidx.lifecycle.compose.LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle, permissionCheckerObserver) {
lifecycle.addObserver(permissionCheckerObserver)
onDispose { lifecycle.removeObserver(permissionCheckerObserver) }
}
}
/**
* Effect that updates the `hasPermission` state of a list of permissions
* when the lifecycle gets called with [lifecycleEvent] and the permission is revoked.
*/
@ExperimentalPermissionsApi
@Composable
internal fun PermissionsLifecycleCheckerEffect(
permissions: List,
lifecycleEvent: Lifecycle.Event = Lifecycle.Event.ON_RESUME
) {
// Check if the permission was granted when the lifecycle is resumed.
// The user might've gone to the Settings screen and granted the permission.
val permissionsCheckerObserver = remember(permissions) {
LifecycleEventObserver { _, event ->
if (event == lifecycleEvent) {
for (permission in permissions) {
// If the permission is revoked, check again. We don't check if the permission
// was denied as that triggers a process restart.
if (permission.status != PermissionStatus.Granted) {
permission.refreshPermissionStatus()
}
}
}
}
}
val lifecycle = androidx.lifecycle.compose.LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle, permissionsCheckerObserver) {
lifecycle.addObserver(permissionsCheckerObserver)
onDispose { lifecycle.removeObserver(permissionsCheckerObserver) }
}
}
/**
* Find the closest Activity in a given Context.
*/
internal fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("Permissions should be called in the context of an Activity")
}
internal fun Context.checkPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) ==
PackageManager.PERMISSION_GRANTED
}
internal fun Activity.shouldShowRationale(permission: String): Boolean {
return ActivityCompat.shouldShowRequestPermissionRationale(this, permission)
}
================================================
FILE: permissions-lint/README.md
================================================
# Lint checks for Permissions for Jetpack Compose
Lint checks for preventing calling `PermissionState.launchPermissionRequest` and
`MultiplePermissionsState.launchMultiplePermissionRequest()` within the Composition as that throws
a runtime exception.
These functions should be called inside a regular lambda or a side-effect but never in the
Composition.
These lint checks will be automatically applied to your project when using
📫 [Permissions](https://google.github.io/accompanist/permissions/).
## Download Permissions for Jetpack Compose
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-permissions:"
}
```
================================================
FILE: permissions-lint/build.gradle.kts
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.google.accompanist.BundleInsideHelper
import com.google.accompanist.BundleInsideHelper.forInsideLintJar
plugins {
`java-library`
id("kotlin")
id(libs.plugins.jetbrains.dokka.get().pluginId)
id(libs.plugins.android.lint.get().pluginId)
}
lint {
htmlReport = true
htmlOutput = file("lint-report.html")
textReport = true
absolutePaths = false
ignoreTestSources = true
}
affectedTestConfiguration {
jvmTestTask = "test"
}
/**
* Creates a configuration for users to use that will be used bundle these dependency
* jars inside of this lint check's jar. This is required because lintPublish does
* not currently support dependencies, so instead we need to bundle any dependencies with the
* lint jar manually. (b/182319899)
*/
val bundleInside = forInsideLintJar()
dependencies {
// Bundle metadataJvm inside the Jar
bundleInside(libs.kotlin.metadataJvm)
compileOnly(libs.android.tools.lint.api)
compileOnly(libs.kotlin.reflect)
compileOnly(libs.kotlin.stdlib)
compileOnly(libs.kotlin.stdlibJdk8) // Override version from transitive dependencies
testImplementation(libs.junit)
testImplementation(libs.kotlin.reflect)
testImplementation(libs.kotlin.stdlib)
testImplementation(libs.kotlin.stdlibJdk8) // Override version from transitive dependencies
testImplementation(libs.android.tools.lint.lint)
testImplementation(libs.android.tools.lint.tests)
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
================================================
FILE: permissions-lint/src/main/java/com/google/accompanist/permissions/lint/PermissionsIssueRegistry.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions.lint
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue
@Suppress("UnstableApiUsage")
public class PermissionsIssueRegistry : IssueRegistry() {
override val issues: List = listOf(
PermissionsLaunchDetector.PermissionLaunchedDuringComposition
)
override val api: Int = 10
override val minApi: Int = CURRENT_API
override val vendor: Vendor = Vendor(
vendorName = "Accompanist Permissions",
identifier = "com.google.accompanist.permissions",
feedbackUrl = "https://github.com/google/accompanist/issues/new/choose"
)
}
================================================
FILE: permissions-lint/src/main/java/com/google/accompanist/permissions/lint/PermissionsLaunchDetector.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
package com.google.accompanist.permissions.lint
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.google.accompanist.permissions.lint.util.Name
import com.google.accompanist.permissions.lint.util.Package
import com.google.accompanist.permissions.lint.util.PackageName
import com.google.accompanist.permissions.lint.util.isInvokedWithinComposable
import com.intellij.psi.PsiJavaFile
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import java.util.EnumSet
/**
* [Detector] that checks `PermissionState.launchPermissionRequest` and
* `MultiplePermissionsState.launchMultiplePermissionRequest` calls to make sure they don't happen
* inside the body of a composable function / lambda.
*/
public class PermissionsLaunchDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames(): List = listOf(
LaunchPermissionRequest.shortName, LaunchMultiplePermissionsRequest.shortName
)
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
if (!method.isInPackageName(PermissionsPackageName)) return
if (node.isInvokedWithinComposable()) {
context.report(
PermissionLaunchedDuringComposition,
node,
context.getNameLocation(node),
"Calls to ${method.name} should happen inside a regular lambda or " +
" a side-effect, but never in the Composition."
)
}
}
public companion object {
public val PermissionLaunchedDuringComposition: Issue = Issue.create(
"PermissionLaunchedDuringComposition",
"Calls to `launchPermissionRequest` or `launchMultiplePermissionRequest` " +
"should happen inside a regular lambda or a side-effect but never in the " +
"Composition.",
"Calls to `launchPermissionRequest` or `launchMultiplePermissionRequest` " +
"in the Composition throw a runtime exception. Please call them inside a regular " +
"lambda or in a side-effect.",
Category.CORRECTNESS, 3, Severity.ERROR,
Implementation(
PermissionsLaunchDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
)
)
}
}
/**
* Returns whether [this] has [packageName] as its package name.
*/
private fun PsiMethod.isInPackageName(packageName: PackageName): Boolean =
packageName.javaPackageName == (containingFile as? PsiJavaFile)?.packageName
private val PermissionsPackageName = Package("com.google.accompanist.permissions")
private val LaunchPermissionRequest =
Name(PermissionsPackageName, "launchPermissionRequest")
private val LaunchMultiplePermissionsRequest =
Name(PermissionsPackageName, "launchMultiplePermissionRequest")
================================================
FILE: permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/ComposableUtils.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("FunctionName")
package com.google.accompanist.permissions.lint.util
// FILE COPIED FROM:
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt
import com.intellij.lang.java.JavaLanguage
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiParameter
import com.intellij.psi.impl.compiled.ClsParameterImpl
import com.intellij.psi.impl.light.LightParameter
import org.jetbrains.kotlin.psi.KtAnnotated
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtTypeReference
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UAnonymousClass
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UDeclaration
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UParameter
import org.jetbrains.uast.UTypeReferenceExpression
import org.jetbrains.uast.UVariable
import org.jetbrains.uast.getContainingDeclaration
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getParameterForArgument
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.withContainingElements
import kotlin.metadata.jvm.annotations
/**
* Returns whether this [UCallExpression] is directly invoked within the body of a Composable
* function or lambda without being `remember`ed.
*/
public fun UCallExpression.isNotRemembered(): Boolean = isNotRememberedWithKeys()
/**
* Returns whether this [UCallExpression] is directly invoked within the body of a Composable
* function or lambda without being `remember`ed, or whether it is invoked inside a `remember call
* without the provided [keys][keyClassNames].
* - Returns true if this [UCallExpression] is directly invoked inside a Composable function or
* lambda without being `remember`ed
* - Returns true if this [UCallExpression] is invoked inside a call to `remember`, but without all
* of the provided [keys][keyClassNames] being used as key parameters to `remember`
* - Returns false if this [UCallExpression] is correctly `remember`ed with the provided
* [keys][keyClassNames], or is not called inside a `remember` block, and is not called inside a
* Composable function or lambda
*
* @param keyClassNames [Name]s representing the expected classes that should be used as a key
* parameter to the `remember` call
*/
public fun UCallExpression.isNotRememberedWithKeys(vararg keyClassNames: Name): Boolean {
val visitor = ComposableBodyVisitor(this)
// The nearest method or lambda expression that contains this call expression
val boundaryElement = visitor.parentUElements().last()
// Check if the nearest lambda expression is actually a call to remember
val rememberCall: UCallExpression? =
(boundaryElement.uastParent as? UCallExpression)?.takeIf {
it.methodName == Names.Runtime.Remember.shortName &&
it.resolve()?.isInPackageName(Names.Runtime.PackageName) == true
}
return if (rememberCall == null) {
visitor.isComposable()
} else {
val parameterTypes =
rememberCall.valueArguments.mapNotNull { it.getExpressionType()?.canonicalText }
!keyClassNames.all { parameterTypes.contains(it.javaFqn) }
}
}
/**
* Returns whether this [UExpression] is invoked within the body of a Composable function or lambda.
*
* This searches parent declarations until we find a lambda expression or a function, and looks to
* see if these are Composable.
*/
fun UExpression.isInvokedWithinComposable(): Boolean {
return ComposableBodyVisitor(this).isComposable()
}
// TODO: https://youtrack.jetbrains.com/issue/KT-45406
// KotlinUMethodWithFakeLightDelegate.hasAnnotation() (for reified functions for example)
// doesn't find annotations, so just look at the annotations directly.
/** Returns whether this method is @Composable or not */
val PsiMethod.isComposable
get() = annotations.any { it.qualifiedName == Names.Runtime.Composable.javaFqn }
/** Returns whether this variable's type is @Composable or not */
val UVariable.isComposable: Boolean
get() {
// Annotation on the lambda
val annotationOnLambda =
when (val initializer = uastInitializer) {
is ULambdaExpression -> {
val source = initializer.sourcePsi
if (source is KtFunction) {
// Anonymous function, val foo = @Composable fun() {}
source.hasComposableAnnotation
} else {
// Lambda, val foo = @Composable {}
initializer.findAnnotation(Names.Runtime.Composable.javaFqn) != null
}
}
else -> false
}
// Annotation on the type, foo: @Composable () -> Unit = { }
val annotationOnType = typeReference?.isComposable == true
return annotationOnLambda || annotationOnType
}
/** Returns whether this parameter's type is @Composable or not */
private val PsiParameter.isComposable: Boolean
get() =
when {
// The parameter is in a class file. Currently type annotations aren't currently added
// to
// the underlying type (https://youtrack.jetbrains.com/issue/KT-45307), so instead we
// use
// the metadata annotation.
this is ClsParameterImpl ||
// In some cases when a method is defined in bytecode and the call fails to resolve
// to the ClsMethodImpl, we will instead get a LightParameter. Note that some Kotlin
// declarations too will also appear as a LightParameter, so we can check to see if
// the source language is Java, which means that this is a LightParameter for
// bytecode, as opposed to for a Kotlin declaration.
// https://youtrack.jetbrains.com/issue/KT-46883
(this is LightParameter && this.language is JavaLanguage) -> {
// Find the containing method, so we can get metadata from the containing class
val containingMethod = getParentOfType(true)
val kmFunction = containingMethod!!.toKmFunction()
val kmValueParameter = kmFunction?.valueParameters?.find { it.name == name }
kmValueParameter?.type?.annotations?.find {
it.className == Names.Runtime.Composable.kmClassName
} != null
}
// The parameter is in a source declaration
else -> (toUElement() as? UParameter)?.typeReference?.isComposable == true
}
/** Returns whether this lambda expression is @Composable or not */
val ULambdaExpression.isComposable: Boolean
get() =
when (val lambdaParent = uastParent) {
// Function call with a lambda parameter
is UCallExpression -> {
val parameter = lambdaParent.getParameterForArgument(this)
parameter?.isComposable == true
}
// A local / non-local lambda variable
is UVariable -> {
lambdaParent.isComposable
}
// Either a new UAST type we haven't handled, or non-Kotlin declarations
else -> false
}
/**
* Helper class that visits parent declarations above the provided [expression], until it finds a
* lambda or method. This 'boundary' is used as the indicator for whether this [expression] can be
* considered to be inside a Composable body or not.
*
* @see isComposable
* @see parentUElements
*/
private class ComposableBodyVisitor(private val expression: UExpression) {
/** @return whether the body can be considered Composable or not */
fun isComposable(): Boolean =
when (val element = parentUElements.last()) {
is UMethod -> element.isComposable
is ULambdaExpression -> element.isComposable
else -> false
}
/** Returns all parent [UElement]s until and including the boundary lambda / method. */
fun parentUElements() = parentUElements
/**
* The outermost UElement that corresponds to the surrounding UDeclaration that contains
* [expression], with the following special cases:
* - if the containing UDeclaration is a local property, we ignore it and search above as it
* still could be created in the context of a Composable body
* - if the containing UDeclaration is an anonymous class (object { }), we ignore it and search
* above as it still could be created in the context of a Composable body
*/
private val boundaryUElement by lazy {
// The nearest property / function / etc declaration that contains this call expression
var containingDeclaration = expression.getContainingDeclaration()
fun UDeclaration.isLocalProperty() = (sourcePsi as? KtProperty)?.isLocal == true
fun UDeclaration.isAnonymousClass() = this is UAnonymousClass
fun UDeclaration.isPropertyInsideAnonymousClass() =
getContainingUClass()?.isAnonymousClass() == true
while (
containingDeclaration != null &&
(
containingDeclaration.isLocalProperty() ||
containingDeclaration.isAnonymousClass() ||
containingDeclaration.isPropertyInsideAnonymousClass()
)
) {
containingDeclaration = containingDeclaration.getContainingDeclaration()
}
containingDeclaration
}
private val parentUElements by lazy {
val elements = mutableListOf()
// Look through containing elements until we find a lambda or a method
for (element in expression.withContainingElements) {
elements += element
when (element) {
// TODO: consider handling the case of a lambda inside an inline function call,
// such as `apply` or `forEach`. These calls don't really change the
// 'composability' here, but there may be other inline function calls that
// capture the lambda and invoke it elsewhere, so we might need to look for
// a callsInPlace contract in the metadata for the function, or the body of the
// source definition.
is ULambdaExpression -> break
is UMethod -> break
// Stop when we reach the parent declaration to avoid escaping the scope.
boundaryUElement -> break
}
}
elements
}
}
/** Returns whether this type reference is @Composable or not */
val UTypeReferenceExpression.isComposable: Boolean
get() {
if (type.hasAnnotation(Names.Runtime.Composable.javaFqn)) return true
// Annotations on the types of local properties (val foo: @Composable () -> Unit = {})
// are currently not present on the PsiType, we so need to manually check the underlying
// type reference. (https://youtrack.jetbrains.com/issue/KTIJ-18821)
return (sourcePsi as? KtTypeReference)?.hasComposableAnnotation == true
}
/** Returns whether this annotated declaration has a Composable annotation */
private val KtAnnotated.hasComposableAnnotation: Boolean
get() =
annotationEntries.any {
(it.toUElement() as UAnnotation).qualifiedName == Names.Runtime.Composable.javaFqn
}
================================================
FILE: permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/KotlinMetadataUtils.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions.lint.util
// FILE COPIED FROM:
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
import com.intellij.lang.jvm.annotation.JvmAnnotationArrayValue
import com.intellij.lang.jvm.annotation.JvmAnnotationAttributeValue
import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiMethod
import com.intellij.psi.impl.compiled.ClsMethodImpl
import com.intellij.psi.util.ClassUtil
import kotlin.metadata.KmDeclarationContainer
import kotlin.metadata.KmFunction
import kotlin.metadata.jvm.KotlinClassMetadata
import kotlin.metadata.jvm.Metadata
import kotlin.metadata.jvm.signature
/**
* @return the corresponding [KmFunction] for this [PsiMethod], or `null` if there is no
* corresponding [KmFunction]. This method is only meaningful if this [PsiMethod] represents a
* method defined in bytecode (most often a [ClsMethodImpl]).
*/
public fun PsiMethod.toKmFunction(): KmFunction? =
containingClass!!.getKmDeclarationContainer()?.findKmFunctionForPsiMethod(this)
// TODO: https://youtrack.jetbrains.com/issue/KT-45310
// Currently there is no built in support for parsing kotlin metadata from kotlin class files, so
// we need to manually inspect the annotations and work with Cls* (compiled PSI).
/**
* Returns the [KmDeclarationContainer] using the kotlin.Metadata annotation present on this
* [PsiClass]. Returns null if there is no annotation (not parsing a Kotlin class file), the
* annotation data is for an unsupported version of Kotlin, or if the metadata represents a
* synthetic class.
*/
private fun PsiClass.getKmDeclarationContainer(): KmDeclarationContainer? {
val classKotlinMetadataPsiAnnotation =
annotations.find {
// hasQualifiedName() not available on the min version of Lint we compile against
it.qualifiedName == KotlinMetadataFqn
} ?: return null
val metadata =
try {
KotlinClassMetadata.readStrict(classKotlinMetadataPsiAnnotation.toMetadataAnnotation())
} catch (e: Exception) {
// Don't crash if we are trying to parse metadata from a newer version of Kotlin, than
// is
// supported by the bundled version of kotlin-metadata-jvm
return null
}
return when (metadata) {
is KotlinClassMetadata.Class -> metadata.kmClass
is KotlinClassMetadata.FileFacade -> metadata.kmPackage
is KotlinClassMetadata.SyntheticClass -> null
is KotlinClassMetadata.MultiFileClassFacade -> null
is KotlinClassMetadata.MultiFileClassPart -> metadata.kmPackage
is KotlinClassMetadata.Unknown -> null
}
}
/** Returns a [Metadata] by parsing the attributes of this @kotlin.Metadata PSI annotation. */
private fun PsiAnnotation.toMetadataAnnotation(): Metadata {
val attributes = attributes.associate { it.attributeName to it.attributeValue }
fun JvmAnnotationAttributeValue.parseString(): String =
(this as JvmAnnotationConstantValue).constantValue as String
fun JvmAnnotationAttributeValue.parseInt(): Int =
(this as JvmAnnotationConstantValue).constantValue as Int
fun JvmAnnotationAttributeValue.parseStringArray(): Array =
(this as JvmAnnotationArrayValue).values.map { it.parseString() }.toTypedArray()
fun JvmAnnotationAttributeValue.parseIntArray(): IntArray =
(this as JvmAnnotationArrayValue).values.map { it.parseInt() }.toTypedArray().toIntArray()
val kind = attributes["k"]?.parseInt()
val metadataVersion = attributes["mv"]?.parseIntArray()
val data1 = attributes["d1"]?.parseStringArray()
val data2 = attributes["d2"]?.parseStringArray()
val extraString = attributes["xs"]?.parseString()
val packageName = attributes["pn"]?.parseString()
val extraInt = attributes["xi"]?.parseInt()
return Metadata(kind, metadataVersion, data1, data2, extraString, packageName, extraInt)
}
/**
* @return the corresponding [KmFunction] in [this] for the given [method], matching by name and
* signature.
*/
private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
// Strip any mangled part of the name in case of value / inline classes
val expectedName = method.name.substringBefore("-")
val expectedSignature = ClassUtil.getAsmMethodSignature(method)
// Since Kotlin 1.6 PSI updates, in some cases what used to be `void` return types are converted
// to `kotlin.Unit`, even though in the actual metadata they are still void. Try to match those
// cases as well
val unitReturnTypeSuffix = "Lkotlin/Unit;"
val expectedSignatureConvertedFromUnitToVoid =
if (expectedSignature.endsWith(unitReturnTypeSuffix)) {
expectedSignature.substringBeforeLast(unitReturnTypeSuffix) + "V"
} else {
expectedSignature
}
return functions.find {
it.name == expectedName &&
(
it.signature?.descriptor == expectedSignature ||
it.signature?.descriptor == expectedSignatureConvertedFromUnitToVoid
)
}
}
private const val KotlinMetadataFqn = "kotlin.Metadata"
================================================
FILE: permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/Names.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions.lint.util
/**
* File copied from
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
*/
import kotlin.metadata.ClassName
/** Contains common names used for lint checks. */
object Names {
object Animation {
val PackageName = Package("androidx.compose.animation")
object Core {
val PackageName = Package("androidx.compose.animation.core")
val Animatable = Name(PackageName, "Animatable")
}
}
object AnimationCore {
val PackageName = Package("androidx.compose.animation.core")
}
object Runtime {
val PackageName = Package("androidx.compose.runtime")
val Composable = Name(PackageName, "Composable")
val CompositionLocal = Name(PackageName, "CompositionLocal")
val DerivedStateOf = Name(PackageName, "derivedStateOf")
val State = Name(PackageName, "State")
val MutableState = Name(PackageName, "MutableState")
val MutableStateOf = Name(PackageName, "mutableStateOf")
val MutableIntStateOf = Name(PackageName, "mutableIntStateOf")
val MutableLongStateOf = Name(PackageName, "mutableLongStateOf")
val MutableFloatStateOf = Name(PackageName, "mutableFloatStateOf")
val MutableDoubleStateOf = Name(PackageName, "mutableDoubleStateOf")
val MutableStateListOf = Name(PackageName, "mutableStateListOf")
val MutableStateMapOf = Name(PackageName, "mutableStateMapOf")
val ProduceState = Name(PackageName, "produceState")
val Remember = Name(PackageName, "remember")
val DisposableEffect = Name(PackageName, "DisposableEffect")
val RememberSaveable = Name(PackageName, "rememberSaveable")
val LaunchedEffect = Name(PackageName, "LaunchedEffect")
val ReusableContent = Name(PackageName, "ReusableContent")
val Key = Name(PackageName, "key")
val StructuralEqualityPolicy = Name(PackageName, "structuralEqualityPolicy")
}
object Ui {
val PackageName = Package("androidx.compose.ui")
val Composed = Name(PackageName, "composed")
val Modifier = Name(PackageName, "Modifier")
object Layout {
val PackageName = Package("androidx.compose.ui.layout")
val ParentDataModifier = Name(PackageName, "ParentDataModifier")
}
object Pointer {
val PackageName = Package(Ui.PackageName, "input.pointer")
val PointerInputScope = Name(PackageName, "PointerInputScope")
val PointerInputScopeModifier = Name(PackageName, "pointerInput")
val AwaitPointerEventScope = Name(PackageName, "awaitPointerEventScope")
}
object Unit {
val PackageName = Package("androidx.compose.ui.unit")
val Dp = Name(PackageName, "Dp")
}
object Node {
val PackageName = Package(Ui.PackageName, "node")
val CurrentValueOf = Name(PackageName, "currentValueOf")
}
}
object UiGraphics {
val PackageName = Package("androidx.compose.ui.graphics")
val Color = Name(PackageName, "Color")
}
}
/**
* Represents a qualified package
*
* @property segments the segments representing the package
*/
class PackageName internal constructor(internal val segments: List) {
/** The Java-style package name for this [Name], separated with `.` */
val javaPackageName: String
get() = segments.joinToString(".")
}
/**
* Represents the qualified name for an element
*
* @property pkg the package for this element
* @property nameSegments the segments representing the element - there can be multiple in the case
* of nested classes.
*/
class Name
internal constructor(private val pkg: PackageName, private val nameSegments: List) {
/** The short name for this [Name] */
val shortName: String
get() = nameSegments.last()
/** The Java-style fully qualified name for this [Name], separated with `.` */
val javaFqn: String
get() = pkg.segments.joinToString(".", postfix = ".") + nameSegments.joinToString(".")
/**
* The [ClassName] for use with kotlin.metadata. Note that in kotlin.metadata the actual type
* might be different from the underlying JVM type, for example: kotlin/Int -> java/lang/Integer
*/
val kmClassName: ClassName
get() = pkg.segments.joinToString("/", postfix = "/") + nameSegments.joinToString(".")
/** The [PackageName] of this element. */
val packageName: PackageName
get() = pkg
}
/** @return a [PackageName] with a Java-style (separated with `.`) [packageName]. */
fun Package(packageName: String): PackageName = PackageName(packageName.split("."))
/** @return a [PackageName] with a Java-style (separated with `.`) [packageName]. */
fun Package(packageName: PackageName, shortName: String): PackageName =
PackageName(packageName.segments + shortName.split("."))
/** @return a [Name] with the provided [pkg] and Java-style (separated with `.`) [shortName]. */
fun Name(pkg: PackageName, shortName: String): Name = Name(pkg, shortName.split("."))
================================================
FILE: permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/PsiUtils.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.permissions.lint.util
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassOwner
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiType
import com.intellij.psi.util.InheritanceUtil
/**
* File copied from
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt
*/
/** Returns whether [this] has [packageName] as its package name. */
fun PsiMethod.isInPackageName(packageName: PackageName): Boolean {
val actual = (containingFile as? PsiClassOwner)?.packageName
return packageName.javaPackageName == actual
}
/** Whether this [PsiMethod] returns Unit */
val PsiMethod.returnsUnit
get() = returnType.isVoidOrUnit
/**
* Whether this [PsiType] is `void` or [Unit]
*
* In Kotlin 1.6 some expressions now explicitly return [Unit] instead of just being [PsiType.VOID],
* so this returns whether this type is either.
*/
val PsiType?.isVoidOrUnit
get() = this == PsiType.VOID || this?.canonicalText == "kotlin.Unit"
/** @return whether [this] inherits from [name]. Returns `true` if [this] _is_ directly [name]. */
fun PsiType.inheritsFrom(name: Name) = InheritanceUtil.isInheritor(this, name.javaFqn)
/** @return whether [this] inherits from [name]. Returns `true` if [this] _is_ directly [name]. */
fun PsiClass.inheritsFrom(name: Name) = InheritanceUtil.isInheritor(this, name.javaFqn)
================================================
FILE: permissions-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
================================================
#
# Copyright 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
com.google.accompanist.permissions.lint.PermissionsIssueRegistry
================================================
FILE: permissions-lint/src/test/java/com/google/accompanist/permissions/lint/PermissionsLaunchDetectorTest.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
package com.google.accompanist.permissions.lint
import com.android.tools.lint.checks.infrastructure.TestFiles
import com.android.tools.lint.checks.infrastructure.TestLintResult
import com.android.tools.lint.checks.infrastructure.TestLintTask
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
/* ktlint-disable max-line-length */
/**
* Test for [PermissionsLaunchDetector].
*/
@RunWith(JUnit4::class)
internal class PermissionsLaunchDetectorTest {
private fun check(fileToAdd: String): TestLintResult {
return TestLintTask.lint()
.files(
LaunchPermissionsStub,
ComposableStub,
TestFiles.kt(fileToAdd)
)
.allowMissingSdk()
.issues(PermissionsLaunchDetector.PermissionLaunchedDuringComposition)
.run()
}
@Test
fun errors() {
check(
"""
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.*
@Composable
fun Test() {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
val lambda = @Composable {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
val lambda2: @Composable () -> Unit = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
@Composable
fun LambdaParameter(content: @Composable () -> Unit) {}
@Composable
fun Test2() {
LambdaParameter(content = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
})
LambdaParameter {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
}
fun test3() {
val localLambda1 = @Composable {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
val localLambda2: @Composable () -> Unit = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
}
"""
)
.expect(
"""
src/test.kt:7: Error: Calls to launchPermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
PermissionState().launchPermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:8: Error: Calls to launchMultiplePermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
MultiplePermissionsState().launchMultiplePermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:12: Error: Calls to launchPermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
PermissionState().launchPermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:13: Error: Calls to launchMultiplePermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
MultiplePermissionsState().launchMultiplePermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:17: Error: Calls to launchPermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
PermissionState().launchPermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:18: Error: Calls to launchMultiplePermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
MultiplePermissionsState().launchMultiplePermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:27: Error: Calls to launchPermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
PermissionState().launchPermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:28: Error: Calls to launchMultiplePermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
MultiplePermissionsState().launchMultiplePermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:31: Error: Calls to launchPermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
PermissionState().launchPermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:32: Error: Calls to launchMultiplePermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
MultiplePermissionsState().launchMultiplePermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:38: Error: Calls to launchPermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
PermissionState().launchPermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:39: Error: Calls to launchMultiplePermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
MultiplePermissionsState().launchMultiplePermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:43: Error: Calls to launchPermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
PermissionState().launchPermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~
src/test.kt:44: Error: Calls to launchMultiplePermissionRequest should happen inside a regular lambda or a side-effect, but never in the Composition. [PermissionLaunchedDuringComposition]
MultiplePermissionsState().launchMultiplePermissionRequest()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 errors, 0 warnings
""".trimIndent()
)
}
@Test
fun noErrors() {
check(
"""
import com.google.accompanist.permissions.*
fun test() {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
val lambda = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
val lambda2: () -> Unit = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
fun lambdaParameter(action: () -> Unit) {}
fun test2() {
lambdaParameter(action = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
})
lambdaParameter {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
}
fun test3() {
val localLambda1 = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
val localLambda2: () -> Unit = {
PermissionState().launchPermissionRequest()
MultiplePermissionsState().launchMultiplePermissionRequest()
}
}
"""
)
.expectClean()
}
}
private val LaunchPermissionsStub = TestFiles.kt(
"com/google/accompanist/permissions/LaunchPermissions.kt",
"""
package com.google.accompanist.permissions
class PermissionState {
fun launchPermissionRequest()
}
class MultiplePermissionsState {
fun launchMultiplePermissionRequest()
}
"""
).indented().within("src")
private val ComposableStub = TestFiles.kt(
"androidx/compose/runtime/Composable.kt",
"""
package androidx.compose.runtime
annotation class Composable
"""
).indented().within("src")
/* ktlint-enable max-line-length */
================================================
FILE: release/signing-cleanup.sh
================================================
#!/bin/sh
# Copyright 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
rm -f release/*.gpg
rm -f release/*.properties
================================================
FILE: release/signing-setup.sh
================================================
#!/bin/bash
# Copyright 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
ENCRYPT_KEY=$1
if [[ ! -z "$ENCRYPT_KEY" ]]; then
# Decrypt GnuPG keyring
openssl aes-256-cbc -md sha256 -d -in release/secring.gpg.aes -out release/secring.gpg -k ${ENCRYPT_KEY}
# Decrypt Play Store key
openssl aes-256-cbc -md sha256 -d -in release/signing.properties.aes -out release/signing.properties -k ${ENCRYPT_KEY}
else
echo "ENCRYPT_KEY is empty"
fi
================================================
FILE: sample/build.gradle.kts
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("UnstableApiUsage")
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.android.kotlin)
alias(libs.plugins.compose.plugin)
}
android {
compileSdk = 35
defaultConfig {
applicationId = "com.google.accompanist.sample"
minSdk = 21
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
buildFeatures {
compose = true
}
buildTypes {
getByName("release") {
signingConfig = signingConfigs.getByName("debug")
}
}
kotlinOptions {
jvmTarget = "17"
}
namespace = "com.google.accompanist.sample"
}
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(project(":adaptive"))
implementation(project(":drawablepainter"))
implementation(project(":permissions"))
implementation(libs.compose.material.iconsext)
implementation(libs.compose.material3.material3)
implementation(libs.compose.foundation.layout)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.ui.util)
implementation(libs.androidx.core)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.kotlin.stdlib)
lintChecks(project(":permissions-lint"))
}
================================================
FILE: sample/src/main/AndroidManifest.xml
================================================
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/ImageLoadingSampleUtils.kt
================================================
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
private val rangeForRandom = (0..100000)
fun randomSampleImageUrl(
seed: Int = rangeForRandom.random(),
width: Int = 300,
height: Int = width,
): String {
return "https://picsum.photos/seed/$seed/$width/$height"
}
/**
* Remember a URL generate by [randomSampleImageUrl].
*/
@Composable
fun rememberRandomSampleImageUrl(
seed: Int = rangeForRandom.random(),
width: Int = 300,
height: Int = width,
): String = remember { randomSampleImageUrl(seed, width, height) }
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/MainActivity.kt
================================================
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
/**
* A list which automatically populates the list of sample activities in this app
* with the category `com.google.accompanist.sample.SAMPLE_CODE`.
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val data = getData(intent.getStringExtra(EXTRA_PATH))
setContent {
AccompanistSampleTheme {
MainScreen(
listData = data,
onItemClick = { startActivity(it) }
)
}
}
}
private fun getData(prefix: String?): List {
val myData = mutableListOf()
val mainIntent = Intent(Intent.ACTION_MAIN, null)
mainIntent.addCategory("com.google.accompanist.sample.SAMPLE_CODE")
@SuppressLint("QueryPermissionsNeeded") // Only querying our own Activities
val list = packageManager.queryIntentActivities(mainIntent, 0)
val prefixPath: Array?
var prefixWithSlash = prefix
if (prefix.isNullOrEmpty()) {
prefixPath = null
} else {
prefixPath = prefix.split("/".toRegex()).toTypedArray()
prefixWithSlash = "$prefix/"
}
val entries = mutableMapOf()
list.forEach { info ->
val labelSeq = info.loadLabel(packageManager)
val label = labelSeq?.toString() ?: info.activityInfo.name
if (prefixWithSlash.isNullOrEmpty() || label.startsWith(prefixWithSlash)) {
val labelPath = label.split("/".toRegex()).toTypedArray()
val nextLabel = if (prefixPath == null) labelPath[0] else labelPath[prefixPath.size]
if ((prefixPath?.size ?: 0) == labelPath.size - 1) {
myData.add(
AccompanistSample(
title = nextLabel,
intent = activityIntent(
info.activityInfo.applicationInfo.packageName,
info.activityInfo.name
)
)
)
} else {
if (entries[nextLabel] == null) {
myData.add(
AccompanistSample(
title = nextLabel,
intent = browseIntent(
if (prefix == "") nextLabel else "$prefix/$nextLabel"
)
)
)
entries[nextLabel] = true
}
}
}
}
myData.sortBy { it.title }
return myData
}
private fun activityIntent(pkg: String, componentName: String): Intent {
val result = Intent()
result.setClassName(pkg, componentName)
return result
}
private fun browseIntent(path: String): Intent {
val result = Intent()
result.setClass(this, MainActivity::class.java)
result.putExtra(EXTRA_PATH, path)
return result
}
}
private const val EXTRA_PATH = "com.example.android.apis.Path"
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/MainScreen.kt
================================================
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
data class AccompanistSample(
val title: String,
val intent: Intent
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
listData: List,
onItemClick: (Intent) -> Unit,
modifier: Modifier = Modifier
) {
Surface(modifier) {
Column {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
scrollBehavior = scrollBehavior
)
ContentList(
listData,
onItemClick,
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
)
.nestedScroll(scrollBehavior.nestedScrollConnection)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ContentList(
listData: List,
onItemClick: (Intent) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(listData) {
ListItem(
headlineText = { Text(it.title) },
modifier = Modifier.clickable { onItemClick(it.intent) }
)
}
item {
Spacer(
Modifier.windowInsetsBottomHeight(
WindowInsets.systemBars
)
)
}
}
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/Theme.kt
================================================
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AccompanistSample(
contentPadding: PaddingValues = PaddingValues(16.dp),
content: @Composable () -> Unit
) {
AccompanistSampleTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier.padding(contentPadding).safeDrawingPadding(),
propagateMinConstraints = true
) {
content()
}
}
}
}
@Composable
fun AccompanistSampleTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme(),
content = content
)
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.adaptive
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy
import com.google.accompanist.adaptive.TwoPane
import com.google.accompanist.adaptive.TwoPaneStrategy
import com.google.accompanist.adaptive.VerticalTwoPaneStrategy
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.google.accompanist.sample.AccompanistSample
class BasicTwoPaneSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
val displayFeatures = calculateDisplayFeatures(this)
TwoPane(
first = {
Card(
modifier = Modifier.padding(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text("First")
}
}
},
second = {
Card(
modifier = Modifier.padding(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text("Second")
}
}
},
strategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates ->
// Split vertically if the height is larger than the width
if (layoutCoordinates.size.height >= layoutCoordinates.size.width) {
VerticalTwoPaneStrategy(
splitFraction = 0.75f
)
} else {
HorizontalTwoPaneStrategy(
splitFraction = 0.75f
)
}.calculateSplitResult(density, layoutDirection, layoutCoordinates)
},
displayFeatures = displayFeatures,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/adaptive/DraggableFoldAwareColumnSample.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.adaptive
import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.FoldAwareColumn
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.google.accompanist.sample.AccompanistSample
import com.google.accompanist.sample.R
import kotlin.math.roundToInt
class DraggableFoldAwareColumnSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
DraggableExample(this@DraggableFoldAwareColumnSample)
}
}
}
}
@Composable
fun DraggableExample(activity: Activity) {
var offset by remember { mutableStateOf(Offset(0f, 0f)) }
FoldAwareColumn(
modifier = Modifier
.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offset = Offset(offset.x + dragAmount.x, offset.y + dragAmount.y)
}
}
.width(400.dp)
.border(5.dp, MaterialTheme.colorScheme.secondary),
displayFeatures = calculateDisplayFeatures(activity),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.border(2.dp, MaterialTheme.colorScheme.primary)
.padding(20.dp)
.align(Alignment.Start),
imageVector = Icons.Default.FavoriteBorder,
contentDescription = null
)
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.border(2.dp, MaterialTheme.colorScheme.primary),
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
)
Image(
modifier = Modifier
.ignoreFold()
.align(Alignment.End)
.border(2.dp, MaterialTheme.colorScheme.primary),
painter = painterResource(id = R.drawable.placeholder),
contentDescription = null
)
}
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/adaptive/HorizontalTwoPaneSample.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.adaptive
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.FoldAwareConfiguration
import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy
import com.google.accompanist.adaptive.TwoPane
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.google.accompanist.sample.AccompanistSample
class HorizontalTwoPaneSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
val displayFeatures = calculateDisplayFeatures(this)
TwoPane(
first = {
Card(
modifier = Modifier.padding(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text("First")
}
}
},
second = {
Card(
modifier = Modifier.padding(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text("Second")
}
}
},
strategy = HorizontalTwoPaneStrategy(
splitFraction = 1f / 3f,
),
displayFeatures = displayFeatures,
foldAwareConfiguration = FoldAwareConfiguration.VerticalFoldsOnly,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/adaptive/NavDrawerFoldAwareColumnSample.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.adaptive
import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.FoldAwareColumn
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.google.accompanist.sample.AccompanistSample
class NavDrawerFoldAwareColumnSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample(contentPadding = PaddingValues(all = 0.dp)) {
NavDrawerExample(this)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavDrawerExample(activity: Activity) {
val icons = listOf(
Icons.Default.Done,
Icons.Default.Face,
Icons.Default.Lock,
Icons.Default.Search,
Icons.Default.ThumbUp,
Icons.Default.Warning,
Icons.Default.Star
)
var selectedIcon by remember { mutableStateOf(icons[0]) }
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet {
FoldAwareColumn(
displayFeatures = calculateDisplayFeatures(activity),
foldPadding = PaddingValues(vertical = 10.dp)
) {
icons.forEach {
NavigationDrawerItem(
modifier = Modifier
.padding(5.dp)
.border(2.dp, MaterialTheme.colorScheme.primary, CircleShape),
icon = { Icon(imageVector = it, contentDescription = it.name) },
label = { Text(it.name.substringAfter('.')) },
selected = it == selectedIcon,
onClick = { selectedIcon = it }
)
}
}
}
},
content = { Surface(modifier = Modifier.fillMaxSize()) {} }
)
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/adaptive/NavRailFoldAwareColumnSample.kt
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.adaptive
import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.FoldAwareColumn
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.google.accompanist.sample.AccompanistSample
class NavRailFoldAwareColumnSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
Row {
NavRail(this@NavRailFoldAwareColumnSample)
Surface(modifier = Modifier.fillMaxSize()) {}
}
}
}
}
}
@Composable
fun NavRail(activity: Activity) {
val icons = listOf(
Icons.Default.Done,
Icons.Default.Face,
Icons.Default.Lock,
Icons.Default.Search,
Icons.Default.ThumbUp,
Icons.Default.Warning,
Icons.Default.Star
)
var selectedIcon by remember { mutableStateOf(icons[0]) }
NavigationRail {
FoldAwareColumn(displayFeatures = calculateDisplayFeatures(activity)) {
icons.forEach {
NavigationRailItem(
modifier = Modifier
.padding(5.dp)
.border(2.dp, MaterialTheme.colorScheme.primary),
selected = it == selectedIcon,
onClick = { selectedIcon = it },
icon = { Icon(imageVector = it, contentDescription = it.name) },
label = { Text(it.name.substringAfter('.')) }
)
}
}
}
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/adaptive/VerticalTwoPaneSample.kt
================================================
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.adaptive
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.FoldAwareConfiguration
import com.google.accompanist.adaptive.TwoPane
import com.google.accompanist.adaptive.VerticalTwoPaneStrategy
import com.google.accompanist.adaptive.calculateDisplayFeatures
import com.google.accompanist.sample.AccompanistSample
class VerticalTwoPaneSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
val displayFeatures = calculateDisplayFeatures(this)
TwoPane(
first = {
Card(
modifier = Modifier.padding(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text("First")
}
}
},
second = {
Card(
modifier = Modifier.padding(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text("Second")
}
}
},
strategy = VerticalTwoPaneStrategy(
splitOffset = 200.dp,
),
displayFeatures = displayFeatures,
foldAwareConfiguration = FoldAwareConfiguration.HorizontalFoldsOnly,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/drawablepainter/DocSamples.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.drawablepainter
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.google.accompanist.sample.R
@Composable
fun BasicSample() {
val drawable = ContextCompat.getDrawable(LocalContext.current, R.drawable.rectangle)
Image(
painter = rememberDrawablePainter(drawable = drawable),
contentDescription = "content description",
)
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/permissions/RequestLocationPermissionsSample.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.permissions
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.sample.AccompanistSample
class RequestLocationPermissionsSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
Sample()
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun Sample() {
val locationPermissionsState = rememberMultiplePermissionsState(
listOf(
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_FINE_LOCATION,
)
)
if (locationPermissionsState.allPermissionsGranted) {
Text("Thanks! I can access your exact location :D")
} else {
Column {
val allPermissionsRevoked =
locationPermissionsState.permissions.size ==
locationPermissionsState.revokedPermissions.size
val textToShow = if (!allPermissionsRevoked) {
// If not all the permissions are revoked, it's because the user accepted the COARSE
// location permission, but not the FINE one.
"Yay! Thanks for letting me access your approximate location. " +
"But you know what would be great? If you allow me to know where you " +
"exactly are. Thank you!"
} else if (locationPermissionsState.shouldShowRationale) {
// Both location permissions have been denied
"Getting your exact location is important for this app. " +
"Please grant us fine location. Thank you :D"
} else {
// First time the user sees this feature or the user doesn't want to be asked again
"This feature requires location permission"
}
val buttonText = if (!allPermissionsRevoked) {
"Allow precise location"
} else {
"Request permissions"
}
Text(text = textToShow)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { locationPermissionsState.launchMultiplePermissionRequest() }) {
Text(buttonText)
}
}
}
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/permissions/RequestMultiplePermissionsSample.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.permissions
import android.Manifest
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.sample.AccompanistSample
@OptIn(ExperimentalPermissionsApi::class)
class RequestMultiplePermissionsSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
val multiplePermissionsState = rememberMultiplePermissionsState(
listOf(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA,
)
)
Sample(multiplePermissionsState)
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun Sample(multiplePermissionsState: MultiplePermissionsState) {
if (multiplePermissionsState.allPermissionsGranted) {
// If all permissions are granted, then show screen with the feature enabled
Text("Camera and Read storage permissions Granted! Thank you!")
} else {
Column {
Text(
getTextToShowGivenPermissions(
multiplePermissionsState.revokedPermissions,
multiplePermissionsState.shouldShowRationale
)
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) {
Text("Request permissions")
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
private fun getTextToShowGivenPermissions(
permissions: List,
shouldShowRationale: Boolean
): String {
val revokedPermissionsSize = permissions.size
if (revokedPermissionsSize == 0) return ""
val textToShow = StringBuilder().apply {
append("The ")
}
for (i in permissions.indices) {
textToShow.append(permissions[i].permission)
when {
revokedPermissionsSize > 1 && i == revokedPermissionsSize - 2 -> {
textToShow.append(", and ")
}
i == revokedPermissionsSize - 1 -> {
textToShow.append(" ")
}
else -> {
textToShow.append(", ")
}
}
}
textToShow.append(if (revokedPermissionsSize == 1) "permission is" else "permissions are")
textToShow.append(
if (shouldShowRationale) {
" important. Please grant all of them for the app to function properly."
} else {
" denied. The app cannot function without them."
}
)
return textToShow.toString()
}
================================================
FILE: sample/src/main/java/com/google/accompanist/sample/permissions/RequestPermissionSample.kt
================================================
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.sample.permissions
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import com.google.accompanist.sample.AccompanistSample
class RequestPermissionSample : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
Sample()
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun Sample() {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (cameraPermissionState.status.isGranted) {
Text("Camera permission Granted")
} else {
Column {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
"The camera is important for this app. Please grant the permission."
} else {
"Camera not available"
}
Text(textToShow)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
================================================
FILE: sample/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: sample/src/main/res/drawable/rectangle.xml
================================================
================================================
FILE: sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: sample/src/main/res/values/strings.xml
================================================
Accompanist Sample
Insets: Basic
Insets: Fragment
Insets: Edge-to-edge list
Insets: Edge-to-edge list with Bottom Navigation
Insets: IME Animations
Horizontal Pager: Basic
Horizontal Pager: Indicator
Horizontal Pager: Transition
Horizontal Pager: Looping
Horizontal Pager: Looping with Indicators
Horizontal Pager: Looping with Tabs
Horizontal Pager: Tabs
Horizontal Pager: Scrolling content
Horizontal Pager: Different paddings
Vertical Pager: Basic
Vertical Pager: Indicator
Pagers: Nested
Navigation: Transitions
Navigation: Bottom Sheets
Permissions: Request permission
Permissions: Request multiple permissions
Permissions: Request location permissions
Flow Layout: Column
Flow Layout: Row
Swipe Refresh: Basic
Swipe Refresh: Edge-to-edge
Swipe Refresh: Tweaked indicator
Swipe Refresh: Custom indicator
Swipe Refresh: VerticalPager
System Ui Controller: Color
System Ui Controller: Color in Dialog
System Ui Controller: Visibility
Placeholder: Basic
Placeholder: Fade
Placeholder: Shimmer
WebView: Basic
WebView: Wrapped Content
WebView: Save State on Navigation
Adaptive: TwoPane Basic
Adaptive: TwoPane Horizontal
Adaptive: TwoPane Vertical
Adaptive: Fold Aware Column with Nav Rail
Adaptive: Fold Aware Column with Nav Drawer
Adaptive: Draggable Fold Aware Column
Test Harness
This is content\n%s
Theme Adapter: Material
Theme Adapter: Material 3
================================================
FILE: sample/src/main/res/values-ar/strings.xml
================================================
هذا مضمون \n%s
================================================
FILE: scripts/run-tests.sh
================================================
#!/bin/bash
# Copyright 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Fail on error and print out commands
set -ex
# By default we don't shard
SHARD_COUNT=0
SHARD_INDEX=0
# By default we don't log
LOG_FILE=""
# By default we run tests on device
DEVICE=true
# Parse parameters
for i in "$@"; do
case $i in
--shard-count=*)
SHARD_COUNT="${i#*=}"
shift
;;
--unit-tests)
DEVICE=false
shift
;;
--shard-index=*)
SHARD_INDEX="${i#*=}"
shift
;;
--log-file=*)
LOG_FILE="${i#*=}"
shift
;;
--run-affected)
RUN_AFFECTED=true
shift
;;
--run-flaky-tests)
RUN_FLAKY=true
shift
;;
--affected-base-ref=*)
BASE_REF="${i#*=}"
shift
;;
*)
echo "Unknown option"
exit 1
;;
esac
done
# Start logcat if we have a file to log to
if [[ ! -z "$LOG_FILE" ]]; then
adb logcat >$LOG_FILE &
fi
FILTER_OPTS=""
# Filter out flaky tests if we're not set to run them
if [[ -z "$RUN_FLAKY" ]]; then
FILTER_OPTS="$FILTER_OPTS -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest"
fi
# If we're set to only run affected test, update the Gradle task
if [[ ! -z "$RUN_AFFECTED" ]]; then
if [ "$DEVICE" = true ]; then
TASK="runAffectedAndroidTests"
else
TASK="runAffectedUnitTests"
fi
TASK="$TASK -Paffected_module_detector.enable"
# If we have a base branch set, add the Gradle property
if [[ ! -z "$BASE_REF" ]]; then
TASK="$TASK -Paffected_base_ref=$BASE_REF"
fi
fi
# If we don't have a task yet, use the defaults
if [[ -z "$TASK" ]]; then
if [ "$DEVICE" = true ]; then
TASK="connectedCheck"
else
TASK="testDebug"
fi
fi
SHARD_OPTS=""
if [ "$SHARD_COUNT" -gt "0" ]; then
# If we have a shard count value, create the necessary Gradle property args.
# We assume that SHARD_INDEX has been set too
SHARD_OPTS="$SHARD_OPTS -Pandroid.testInstrumentationRunnerArguments.numShards=$SHARD_COUNT"
SHARD_OPTS="$SHARD_OPTS -Pandroid.testInstrumentationRunnerArguments.shardIndex=$SHARD_INDEX"
fi
./gradlew --scan --continue --no-configuration-cache --stacktrace $TASK $FILTER_OPTS $SHARD_OPTS
================================================
FILE: settings.gradle.kts
================================================
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
include(":adaptive")
include(":internal-testutils")
include(":drawablepainter")
include(":permissions")
include(":permissions-lint")
include(":sample")
rootProject.name = "accompanist"
check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
"""
Accompanist requires JDK 17+ but it is currently using JDK ${JavaVersion.current()}.
Java Home: [${System.getProperty("java.home")}]
https://developer.android.com/build/jdks#jdk-config-in-studio
""".trimIndent()
}
================================================
FILE: spotless/copyright.txt
================================================
/*
* Copyright $YEAR The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
================================================
FILE: spotless/greclipse.properties
================================================
#
# Copyright 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#Whether to use 'space', 'tab' or 'mixed' (both) characters for indentation.
#The default value is 'tab'.
org.eclipse.jdt.core.formatter.tabulation.char=space
#Number of spaces used for indentation in case 'space' characters
#have been selected. The default value is 4.
org.eclipse.jdt.core.formatter.tabulation.size=4
#Number of spaces used for indentation in case 'mixed' characters
#have been selected. The default value is 4.
org.eclipse.jdt.core.formatter.indentation.size=4
#Whether or not indentation characters are inserted into empty lines.
#The default value is 'true'.
org.eclipse.jdt.core.formatter.indent_empty_lines=false
#Number of spaces used for multiline indentation.
#The default value is 2.
groovy.formatter.multiline.indentation=2
#Length after which list are considered too long. These will be wrapped.
#The default value is 30.
groovy.formatter.longListLength=30
#Whether opening braces position shall be the next line.
#The default value is 'same'.
groovy.formatter.braces.start=same
#Whether closing braces position shall be the next line.
#The default value is 'next'.
groovy.formatter.braces.end=next
#Remove unnecessary semicolons. The default value is 'false'.
groovy.formatter.remove.unnecessary.semicolons=false