@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: Pull request
|
|
||||||
about: Create a pull request
|
|
||||||
label: 'triage me'
|
|
||||||
---
|
|
||||||
Thank you for opening a Pull Request!
|
|
||||||
Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
|
|
||||||
- [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
|
|
||||||
- [ ] Ensure the tests and linter pass (`./gradlew --init-script gradle/init.gradle.kts spotlessApply` to automatically apply formatting)
|
|
||||||
- [ ] Appropriate docs were updated (if necessary)
|
|
||||||
|
|
||||||
Is this your first Pull Request?
|
|
||||||
- [ ] Run `./tools/setup.sh`
|
|
||||||
- [ ] Import the code formatting style as explained in [the setup script](/tools/setup.sh#L40).
|
|
||||||
|
|
||||||
Fixes #<issue_number_goes_here> 🦕
|
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "gradle"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
registries: "*"
|
||||||
|
labels: [ "version update" ]
|
||||||
|
groups:
|
||||||
|
kotlin-ksp:
|
||||||
|
patterns:
|
||||||
|
- "org.jetbrains.kotlin:*"
|
||||||
|
- "org.jetbrains.kotlin.jvm"
|
||||||
|
- "com.google.devtools.ksp"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
registries:
|
||||||
|
maven-google:
|
||||||
|
type: "maven-repository"
|
||||||
|
url: "https://maven.google.com"
|
||||||
|
replaces-base: true
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
**DO NOT CREATE A PULL REQUEST WITHOUT READING THESE INSTRUCTIONS**
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
Thanks for submitting a pull request. To accept your pull request we need you do a few things:
|
||||||
|
|
||||||
|
**If this is your first pull request**
|
||||||
|
|
||||||
|
- [Sign the contributors license agreement](https://cla.developers.google.com/)
|
||||||
|
|
||||||
|
**Ensure tests pass and code is formatted correctly**
|
||||||
|
|
||||||
|
- Run local tests on the `DemoDebug` variant by running `./gradlew testDemoDebug`
|
||||||
|
- Fix code formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply`
|
||||||
|
|
||||||
|
**Add a description**
|
||||||
|
|
||||||
|
We need to know what you've done and why you've done it. Include a summary of what your pull request contains, and why you have made these changes. Include links to any relevant issues which it fixes.
|
||||||
|
|
||||||
|
[Here's an example](https://github.com/android/nowinandroid/pull/1257).
|
||||||
|
|
||||||
|
**NOW DELETE THIS LINE AND EVERYTHING ABOVE IT**
|
||||||
|
|
||||||
|
**What I have done and why**
|
||||||
|
|
||||||
|
\<add your PR description here\>
|
||||||
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:base", "group:all", ":dependencyDashboard", "schedule:daily"
|
|
||||||
],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchPackageNames": ["org.objenesis:objenesis"],
|
|
||||||
"allowedVersions": "<=2.6"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackageNames": ["com.google.protobuf"],
|
|
||||||
"allowedVersions": "<=0.8.19"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
name: NightlyBaselineProfiles
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '42 4 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
baseline_profiles:
|
||||||
|
name: "Generate Baseline Profiles"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Enable KVM group perms
|
||||||
|
run: |
|
||||||
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger --name-match=kvm
|
||||||
|
ls /dev/kvm
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: 17
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Accept licenses
|
||||||
|
run: yes | sdkmanager --licenses || true
|
||||||
|
|
||||||
|
- name: Check build-logic
|
||||||
|
run: ./gradlew check -p build-logic
|
||||||
|
|
||||||
|
- name: Setup GMD
|
||||||
|
run: ./gradlew :benchmarks:pixel6Api33Setup
|
||||||
|
--info
|
||||||
|
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||||
|
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||||
|
|
||||||
|
- name: Build all build type and flavor permutations including baseline profiles
|
||||||
|
run: ./gradlew :app:assemble
|
||||||
|
-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=baselineprofile
|
||||||
|
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||||
|
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||||
@ -1,2 +1,2 @@
|
|||||||
# This file can be used to trigger an internal build by changing the number below
|
# This file can be used to trigger an internal build by changing the number below
|
||||||
3
|
2
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
# :app-nia-catalog module
|
# :app-nia-catalog module
|
||||||
|
## Dependency graph
|
||||||

|

|
||||||
|
|||||||
@ -0,0 +1,123 @@
|
|||||||
|
androidx.activity:activity-compose:1.9.3
|
||||||
|
androidx.activity:activity-ktx:1.9.3
|
||||||
|
androidx.activity:activity:1.9.3
|
||||||
|
androidx.annotation:annotation-experimental:1.4.1
|
||||||
|
androidx.annotation:annotation-jvm:1.8.1
|
||||||
|
androidx.annotation:annotation:1.8.1
|
||||||
|
androidx.appcompat:appcompat-resources:1.6.1
|
||||||
|
androidx.arch.core:core-common:2.2.0
|
||||||
|
androidx.arch.core:core-runtime:2.2.0
|
||||||
|
androidx.autofill:autofill:1.0.0
|
||||||
|
androidx.browser:browser:1.8.0
|
||||||
|
androidx.collection:collection-jvm:1.4.4
|
||||||
|
androidx.collection:collection-ktx:1.4.4
|
||||||
|
androidx.collection:collection:1.4.4
|
||||||
|
androidx.compose.animation:animation-android:1.7.5
|
||||||
|
androidx.compose.animation:animation-core-android:1.7.5
|
||||||
|
androidx.compose.animation:animation-core:1.7.5
|
||||||
|
androidx.compose.animation:animation:1.7.5
|
||||||
|
androidx.compose.foundation:foundation-android:1.7.5
|
||||||
|
androidx.compose.foundation:foundation-layout-android:1.7.5
|
||||||
|
androidx.compose.foundation:foundation-layout:1.7.5
|
||||||
|
androidx.compose.foundation:foundation:1.7.5
|
||||||
|
androidx.compose.material3.adaptive:adaptive-android:1.0.0
|
||||||
|
androidx.compose.material3.adaptive:adaptive:1.0.0
|
||||||
|
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
|
||||||
|
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
|
||||||
|
androidx.compose.material3:material3-android:1.3.1
|
||||||
|
androidx.compose.material3:material3:1.3.1
|
||||||
|
androidx.compose.material:material-icons-core-android:1.7.5
|
||||||
|
androidx.compose.material:material-icons-core:1.7.5
|
||||||
|
androidx.compose.material:material-icons-extended-android:1.7.5
|
||||||
|
androidx.compose.material:material-icons-extended:1.7.5
|
||||||
|
androidx.compose.material:material-ripple-android:1.7.5
|
||||||
|
androidx.compose.material:material-ripple:1.7.5
|
||||||
|
androidx.compose.runtime:runtime-android:1.7.5
|
||||||
|
androidx.compose.runtime:runtime-saveable-android:1.7.5
|
||||||
|
androidx.compose.runtime:runtime-saveable:1.7.5
|
||||||
|
androidx.compose.runtime:runtime:1.7.5
|
||||||
|
androidx.compose.ui:ui-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-geometry-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-geometry:1.7.5
|
||||||
|
androidx.compose.ui:ui-graphics-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-graphics:1.7.5
|
||||||
|
androidx.compose.ui:ui-text-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-text:1.7.5
|
||||||
|
androidx.compose.ui:ui-tooling-preview-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-tooling-preview:1.7.5
|
||||||
|
androidx.compose.ui:ui-unit-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-unit:1.7.5
|
||||||
|
androidx.compose.ui:ui-util-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-util:1.7.5
|
||||||
|
androidx.compose.ui:ui:1.7.5
|
||||||
|
androidx.compose:compose-bom:2024.11.00
|
||||||
|
androidx.concurrent:concurrent-futures:1.1.0
|
||||||
|
androidx.core:core-ktx:1.13.1
|
||||||
|
androidx.core:core:1.13.1
|
||||||
|
androidx.customview:customview-poolingcontainer:1.0.0
|
||||||
|
androidx.customview:customview:1.0.0
|
||||||
|
androidx.emoji2:emoji2:1.3.0
|
||||||
|
androidx.exifinterface:exifinterface:1.3.7
|
||||||
|
androidx.fragment:fragment:1.5.1
|
||||||
|
androidx.graphics:graphics-path:1.0.1
|
||||||
|
androidx.interpolator:interpolator:1.0.0
|
||||||
|
androidx.lifecycle:lifecycle-common-java8:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-common-jvm:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-common:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-livedata-core:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-livedata:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-process:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-runtime-android:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-runtime-compose:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-runtime-ktx:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-runtime:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-android:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.3
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel:2.8.3
|
||||||
|
androidx.loader:loader:1.0.0
|
||||||
|
androidx.metrics:metrics-performance:1.0.0-beta01
|
||||||
|
androidx.profileinstaller:profileinstaller:1.3.1
|
||||||
|
androidx.savedstate:savedstate-ktx:1.2.1
|
||||||
|
androidx.savedstate:savedstate:1.2.1
|
||||||
|
androidx.startup:startup-runtime:1.1.1
|
||||||
|
androidx.tracing:tracing-ktx:1.3.0-alpha02
|
||||||
|
androidx.tracing:tracing:1.3.0-alpha02
|
||||||
|
androidx.vectordrawable:vectordrawable-animated:1.1.0
|
||||||
|
androidx.vectordrawable:vectordrawable:1.1.0
|
||||||
|
androidx.versionedparcelable:versionedparcelable:1.1.1
|
||||||
|
androidx.viewpager:viewpager:1.0.0
|
||||||
|
androidx.window.extensions.core:core:1.0.0
|
||||||
|
androidx.window:window-core-android:1.3.0
|
||||||
|
androidx.window:window-core:1.3.0
|
||||||
|
androidx.window:window:1.3.0
|
||||||
|
com.google.accompanist:accompanist-drawablepainter:0.32.0
|
||||||
|
com.google.code.findbugs:jsr305:3.0.2
|
||||||
|
com.google.dagger:dagger-lint-aar:2.52
|
||||||
|
com.google.dagger:dagger:2.52
|
||||||
|
com.google.dagger:hilt-android:2.52
|
||||||
|
com.google.dagger:hilt-core:2.52
|
||||||
|
com.google.guava:listenablefuture:1.0
|
||||||
|
com.squareup.okhttp3:okhttp:4.12.0
|
||||||
|
com.squareup.okio:okio-jvm:3.9.0
|
||||||
|
com.squareup.okio:okio:3.9.0
|
||||||
|
io.coil-kt:coil-base:2.7.0
|
||||||
|
io.coil-kt:coil-compose-base:2.7.0
|
||||||
|
io.coil-kt:coil-compose:2.7.0
|
||||||
|
io.coil-kt:coil:2.7.0
|
||||||
|
jakarta.inject:jakarta.inject-api:2.0.1
|
||||||
|
javax.inject:javax.inject:1
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib:2.0.20
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1
|
||||||
|
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1
|
||||||
|
org.jetbrains.kotlinx:kotlinx-datetime:0.6.1
|
||||||
|
org.jetbrains:annotations:23.0.0
|
||||||
@ -1,3 +1,3 @@
|
|||||||
# :app module
|
# :app module
|
||||||
|
## Dependency graph
|
||||||

|

|
||||||
|
|||||||
@ -0,0 +1,232 @@
|
|||||||
|
androidx.activity:activity-compose:1.9.3
|
||||||
|
androidx.activity:activity-ktx:1.9.3
|
||||||
|
androidx.activity:activity:1.9.3
|
||||||
|
androidx.annotation:annotation-experimental:1.4.1
|
||||||
|
androidx.annotation:annotation-jvm:1.8.1
|
||||||
|
androidx.annotation:annotation:1.8.1
|
||||||
|
androidx.appcompat:appcompat-resources:1.7.0
|
||||||
|
androidx.appcompat:appcompat:1.7.0
|
||||||
|
androidx.arch.core:core-common:2.2.0
|
||||||
|
androidx.arch.core:core-runtime:2.2.0
|
||||||
|
androidx.autofill:autofill:1.0.0
|
||||||
|
androidx.browser:browser:1.8.0
|
||||||
|
androidx.collection:collection-jvm:1.4.4
|
||||||
|
androidx.collection:collection-ktx:1.4.4
|
||||||
|
androidx.collection:collection:1.4.4
|
||||||
|
androidx.compose.animation:animation-android:1.7.5
|
||||||
|
androidx.compose.animation:animation-core-android:1.7.5
|
||||||
|
androidx.compose.animation:animation-core:1.7.5
|
||||||
|
androidx.compose.animation:animation:1.7.5
|
||||||
|
androidx.compose.foundation:foundation-android:1.7.5
|
||||||
|
androidx.compose.foundation:foundation-layout-android:1.7.5
|
||||||
|
androidx.compose.foundation:foundation-layout:1.7.5
|
||||||
|
androidx.compose.foundation:foundation:1.7.5
|
||||||
|
androidx.compose.material3.adaptive:adaptive-android:1.0.0
|
||||||
|
androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0
|
||||||
|
androidx.compose.material3.adaptive:adaptive-layout:1.0.0
|
||||||
|
androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0
|
||||||
|
androidx.compose.material3.adaptive:adaptive-navigation:1.0.0
|
||||||
|
androidx.compose.material3.adaptive:adaptive:1.0.0
|
||||||
|
androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1
|
||||||
|
androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1
|
||||||
|
androidx.compose.material3:material3-android:1.3.1
|
||||||
|
androidx.compose.material3:material3-window-size-class-android:1.3.1
|
||||||
|
androidx.compose.material3:material3-window-size-class:1.3.1
|
||||||
|
androidx.compose.material3:material3:1.3.1
|
||||||
|
androidx.compose.material:material-icons-core-android:1.7.5
|
||||||
|
androidx.compose.material:material-icons-core:1.7.5
|
||||||
|
androidx.compose.material:material-icons-extended-android:1.7.5
|
||||||
|
androidx.compose.material:material-icons-extended:1.7.5
|
||||||
|
androidx.compose.material:material-ripple-android:1.7.5
|
||||||
|
androidx.compose.material:material-ripple:1.7.5
|
||||||
|
androidx.compose.runtime:runtime-android:1.7.5
|
||||||
|
androidx.compose.runtime:runtime-saveable-android:1.7.5
|
||||||
|
androidx.compose.runtime:runtime-saveable:1.7.5
|
||||||
|
androidx.compose.runtime:runtime-tracing:1.7.5
|
||||||
|
androidx.compose.runtime:runtime:1.7.5
|
||||||
|
androidx.compose.ui:ui-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-geometry-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-geometry:1.7.5
|
||||||
|
androidx.compose.ui:ui-graphics-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-graphics:1.7.5
|
||||||
|
androidx.compose.ui:ui-text-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-text:1.7.5
|
||||||
|
androidx.compose.ui:ui-tooling-preview-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-tooling-preview:1.7.5
|
||||||
|
androidx.compose.ui:ui-unit-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-unit:1.7.5
|
||||||
|
androidx.compose.ui:ui-util-android:1.7.5
|
||||||
|
androidx.compose.ui:ui-util:1.7.5
|
||||||
|
androidx.compose.ui:ui:1.7.5
|
||||||
|
androidx.compose:compose-bom:2024.11.00
|
||||||
|
androidx.concurrent:concurrent-futures:1.1.0
|
||||||
|
androidx.core:core-ktx:1.13.1
|
||||||
|
androidx.core:core-splashscreen:1.0.1
|
||||||
|
androidx.core:core:1.13.1
|
||||||
|
androidx.cursoradapter:cursoradapter:1.0.0
|
||||||
|
androidx.customview:customview-poolingcontainer:1.0.0
|
||||||
|
androidx.customview:customview:1.0.0
|
||||||
|
androidx.datastore:datastore-android:1.1.1
|
||||||
|
androidx.datastore:datastore-core-android:1.1.1
|
||||||
|
androidx.datastore:datastore-core-okio-jvm:1.1.1
|
||||||
|
androidx.datastore:datastore-core-okio:1.1.1
|
||||||
|
androidx.datastore:datastore-core:1.1.1
|
||||||
|
androidx.datastore:datastore-preferences-android:1.1.1
|
||||||
|
androidx.datastore:datastore-preferences-core-jvm:1.1.1
|
||||||
|
androidx.datastore:datastore-preferences-core:1.1.1
|
||||||
|
androidx.datastore:datastore-preferences:1.1.1
|
||||||
|
androidx.datastore:datastore:1.1.1
|
||||||
|
androidx.documentfile:documentfile:1.0.0
|
||||||
|
androidx.drawerlayout:drawerlayout:1.0.0
|
||||||
|
androidx.emoji2:emoji2-views-helper:1.3.0
|
||||||
|
androidx.emoji2:emoji2:1.3.0
|
||||||
|
androidx.exifinterface:exifinterface:1.3.7
|
||||||
|
androidx.fragment:fragment:1.5.4
|
||||||
|
androidx.graphics:graphics-path:1.0.1
|
||||||
|
androidx.hilt:hilt-common:1.2.0
|
||||||
|
androidx.hilt:hilt-navigation-compose:1.2.0
|
||||||
|
androidx.hilt:hilt-navigation:1.2.0
|
||||||
|
androidx.hilt:hilt-work:1.2.0
|
||||||
|
androidx.interpolator:interpolator:1.0.0
|
||||||
|
androidx.legacy:legacy-support-core-utils:1.0.0
|
||||||
|
androidx.lifecycle:lifecycle-common-java8:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-common-jvm:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-common:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-livedata-core:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-livedata:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-process:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-runtime-android:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-runtime-compose-android:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-runtime-compose:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-runtime-ktx:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-runtime:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-service:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-android:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6
|
||||||
|
androidx.lifecycle:lifecycle-viewmodel:2.8.6
|
||||||
|
androidx.loader:loader:1.0.0
|
||||||
|
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
|
||||||
|
androidx.metrics:metrics-performance:1.0.0-beta01
|
||||||
|
androidx.navigation:navigation-common-ktx:2.8.0
|
||||||
|
androidx.navigation:navigation-common:2.8.0
|
||||||
|
androidx.navigation:navigation-compose:2.8.0
|
||||||
|
androidx.navigation:navigation-runtime-ktx:2.8.0
|
||||||
|
androidx.navigation:navigation-runtime:2.8.0
|
||||||
|
androidx.print:print:1.0.0
|
||||||
|
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
|
||||||
|
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
|
||||||
|
androidx.profileinstaller:profileinstaller:1.3.1
|
||||||
|
androidx.resourceinspection:resourceinspection-annotation:1.0.1
|
||||||
|
androidx.room:room-common:2.6.1
|
||||||
|
androidx.room:room-ktx:2.6.1
|
||||||
|
androidx.room:room-runtime:2.6.1
|
||||||
|
androidx.savedstate:savedstate-ktx:1.2.1
|
||||||
|
androidx.savedstate:savedstate:1.2.1
|
||||||
|
androidx.sqlite:sqlite-framework:2.4.0
|
||||||
|
androidx.sqlite:sqlite:2.4.0
|
||||||
|
androidx.startup:startup-runtime:1.1.1
|
||||||
|
androidx.tracing:tracing-ktx:1.3.0-alpha02
|
||||||
|
androidx.tracing:tracing-perfetto:1.0.0
|
||||||
|
androidx.tracing:tracing:1.3.0-alpha02
|
||||||
|
androidx.vectordrawable:vectordrawable-animated:1.1.0
|
||||||
|
androidx.vectordrawable:vectordrawable:1.1.0
|
||||||
|
androidx.versionedparcelable:versionedparcelable:1.1.1
|
||||||
|
androidx.viewpager:viewpager:1.0.0
|
||||||
|
androidx.window.extensions.core:core:1.0.0
|
||||||
|
androidx.window:window-core-android:1.3.0
|
||||||
|
androidx.window:window-core:1.3.0
|
||||||
|
androidx.window:window:1.3.0
|
||||||
|
androidx.work:work-runtime-ktx:2.9.0
|
||||||
|
androidx.work:work-runtime:2.9.0
|
||||||
|
com.caverock:androidsvg-aar:1.4
|
||||||
|
com.google.accompanist:accompanist-drawablepainter:0.32.0
|
||||||
|
com.google.accompanist:accompanist-permissions:0.36.0
|
||||||
|
com.google.android.datatransport:transport-api:3.2.0
|
||||||
|
com.google.android.datatransport:transport-backend-cct:3.3.0
|
||||||
|
com.google.android.datatransport:transport-runtime:3.3.0
|
||||||
|
com.google.android.gms:play-services-ads-identifier:18.0.0
|
||||||
|
com.google.android.gms:play-services-base:18.5.0
|
||||||
|
com.google.android.gms:play-services-basement:18.4.0
|
||||||
|
com.google.android.gms:play-services-cloud-messaging:17.2.0
|
||||||
|
com.google.android.gms:play-services-measurement-api:22.1.0
|
||||||
|
com.google.android.gms:play-services-measurement-base:22.1.0
|
||||||
|
com.google.android.gms:play-services-measurement-impl:22.1.0
|
||||||
|
com.google.android.gms:play-services-measurement-sdk-api:22.1.0
|
||||||
|
com.google.android.gms:play-services-measurement-sdk:22.1.0
|
||||||
|
com.google.android.gms:play-services-measurement:22.1.0
|
||||||
|
com.google.android.gms:play-services-oss-licenses:17.1.0
|
||||||
|
com.google.android.gms:play-services-stats:17.0.2
|
||||||
|
com.google.android.gms:play-services-tasks:18.2.0
|
||||||
|
com.google.code.findbugs:jsr305:3.0.2
|
||||||
|
com.google.dagger:dagger-lint-aar:2.52
|
||||||
|
com.google.dagger:dagger:2.52
|
||||||
|
com.google.dagger:hilt-android:2.52
|
||||||
|
com.google.dagger:hilt-core:2.52
|
||||||
|
com.google.errorprone:error_prone_annotations:2.26.0
|
||||||
|
com.google.firebase:firebase-abt:21.1.1
|
||||||
|
com.google.firebase:firebase-analytics:22.1.0
|
||||||
|
com.google.firebase:firebase-annotations:16.2.0
|
||||||
|
com.google.firebase:firebase-bom:33.3.0
|
||||||
|
com.google.firebase:firebase-common-ktx:21.0.0
|
||||||
|
com.google.firebase:firebase-common:21.0.0
|
||||||
|
com.google.firebase:firebase-components:18.0.0
|
||||||
|
com.google.firebase:firebase-config-interop:16.0.1
|
||||||
|
com.google.firebase:firebase-config:22.0.0
|
||||||
|
com.google.firebase:firebase-crashlytics:19.1.0
|
||||||
|
com.google.firebase:firebase-datatransport:19.0.0
|
||||||
|
com.google.firebase:firebase-encoders-json:18.0.1
|
||||||
|
com.google.firebase:firebase-encoders-proto:16.0.0
|
||||||
|
com.google.firebase:firebase-encoders:17.0.0
|
||||||
|
com.google.firebase:firebase-iid-interop:17.1.0
|
||||||
|
com.google.firebase:firebase-installations-interop:17.2.0
|
||||||
|
com.google.firebase:firebase-installations:18.0.0
|
||||||
|
com.google.firebase:firebase-measurement-connector:20.0.1
|
||||||
|
com.google.firebase:firebase-messaging:24.0.1
|
||||||
|
com.google.firebase:firebase-perf:21.0.1
|
||||||
|
com.google.firebase:firebase-sessions:2.0.4
|
||||||
|
com.google.firebase:protolite-well-known-types:18.0.0
|
||||||
|
com.google.guava:failureaccess:1.0.1
|
||||||
|
com.google.guava:guava:31.1-android
|
||||||
|
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|
||||||
|
com.google.j2objc:j2objc-annotations:1.3
|
||||||
|
com.google.protobuf:protobuf-javalite:4.28.2
|
||||||
|
com.google.protobuf:protobuf-kotlin-lite:4.28.2
|
||||||
|
com.squareup.okhttp3:logging-interceptor:4.12.0
|
||||||
|
com.squareup.okhttp3:okhttp:4.12.0
|
||||||
|
com.squareup.okio:okio-jvm:3.9.0
|
||||||
|
com.squareup.okio:okio:3.9.0
|
||||||
|
com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0
|
||||||
|
com.squareup.retrofit2:retrofit:2.11.0
|
||||||
|
io.coil-kt:coil-base:2.7.0
|
||||||
|
io.coil-kt:coil-compose-base:2.7.0
|
||||||
|
io.coil-kt:coil-compose:2.7.0
|
||||||
|
io.coil-kt:coil-svg:2.7.0
|
||||||
|
io.coil-kt:coil:2.7.0
|
||||||
|
jakarta.inject:jakarta.inject-api:2.0.1
|
||||||
|
javax.inject:javax.inject:1
|
||||||
|
org.checkerframework:checker-qual:3.12.0
|
||||||
|
org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.22
|
||||||
|
org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
|
||||||
|
org.jetbrains.kotlin:kotlin-stdlib:2.0.20
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.9.0
|
||||||
|
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0
|
||||||
|
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1
|
||||||
|
org.jetbrains.kotlinx:kotlinx-datetime:0.6.1
|
||||||
|
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3
|
||||||
|
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3
|
||||||
|
org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3
|
||||||
|
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3
|
||||||
|
org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3
|
||||||
|
org.jetbrains:annotations:23.0.0
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14'
|
||||||
|
sdkVersion:'21'
|
||||||
|
targetSdkVersion:'34'
|
||||||
|
uses-permission: name='android.permission.INTERNET'
|
||||||
|
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
|
||||||
|
uses-permission: name='android.permission.POST_NOTIFICATIONS'
|
||||||
|
uses-permission: name='android.permission.WAKE_LOCK'
|
||||||
|
uses-permission: name='com.google.android.c2dm.permission.RECEIVE'
|
||||||
|
uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE'
|
||||||
|
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
|
||||||
|
uses-permission: name='android.permission.FOREGROUND_SERVICE'
|
||||||
|
uses-permission: name='com.google.samples.apps.nowinandroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'
|
||||||
|
application-label:'Now in Android'
|
||||||
|
application-label-af:'Now in Android'
|
||||||
|
application-label-am:'Now in Android'
|
||||||
|
application-label-ar:'Now in Android'
|
||||||
|
application-label-as:'Now in Android'
|
||||||
|
application-label-az:'Now in Android'
|
||||||
|
application-label-be:'Now in Android'
|
||||||
|
application-label-bg:'Now in Android'
|
||||||
|
application-label-bn:'Now in Android'
|
||||||
|
application-label-bs:'Now in Android'
|
||||||
|
application-label-ca:'Now in Android'
|
||||||
|
application-label-cs:'Now in Android'
|
||||||
|
application-label-da:'Now in Android'
|
||||||
|
application-label-de:'Now in Android'
|
||||||
|
application-label-el:'Now in Android'
|
||||||
|
application-label-en-AU:'Now in Android'
|
||||||
|
application-label-en-CA:'Now in Android'
|
||||||
|
application-label-en-GB:'Now in Android'
|
||||||
|
application-label-en-IN:'Now in Android'
|
||||||
|
application-label-en-XC:'Now in Android'
|
||||||
|
application-label-es:'Now in Android'
|
||||||
|
application-label-es-US:'Now in Android'
|
||||||
|
application-label-et:'Now in Android'
|
||||||
|
application-label-eu:'Now in Android'
|
||||||
|
application-label-fa:'Now in Android'
|
||||||
|
application-label-fi:'Now in Android'
|
||||||
|
application-label-fr:'Now in Android'
|
||||||
|
application-label-fr-CA:'Now in Android'
|
||||||
|
application-label-gl:'Now in Android'
|
||||||
|
application-label-gu:'Now in Android'
|
||||||
|
application-label-hi:'Now in Android'
|
||||||
|
application-label-hr:'Now in Android'
|
||||||
|
application-label-hu:'Now in Android'
|
||||||
|
application-label-hy:'Now in Android'
|
||||||
|
application-label-in:'Now in Android'
|
||||||
|
application-label-is:'Now in Android'
|
||||||
|
application-label-it:'Now in Android'
|
||||||
|
application-label-iw:'Now in Android'
|
||||||
|
application-label-ja:'Now in Android'
|
||||||
|
application-label-ka:'Now in Android'
|
||||||
|
application-label-kk:'Now in Android'
|
||||||
|
application-label-km:'Now in Android'
|
||||||
|
application-label-kn:'Now in Android'
|
||||||
|
application-label-ko:'Now in Android'
|
||||||
|
application-label-ky:'Now in Android'
|
||||||
|
application-label-lo:'Now in Android'
|
||||||
|
application-label-lt:'Now in Android'
|
||||||
|
application-label-lv:'Now in Android'
|
||||||
|
application-label-mk:'Now in Android'
|
||||||
|
application-label-ml:'Now in Android'
|
||||||
|
application-label-mn:'Now in Android'
|
||||||
|
application-label-mr:'Now in Android'
|
||||||
|
application-label-ms:'Now in Android'
|
||||||
|
application-label-my:'Now in Android'
|
||||||
|
application-label-nb:'Now in Android'
|
||||||
|
application-label-ne:'Now in Android'
|
||||||
|
application-label-nl:'Now in Android'
|
||||||
|
application-label-or:'Now in Android'
|
||||||
|
application-label-pa:'Now in Android'
|
||||||
|
application-label-pl:'Now in Android'
|
||||||
|
application-label-pt:'Now in Android'
|
||||||
|
application-label-pt-BR:'Now in Android'
|
||||||
|
application-label-pt-PT:'Now in Android'
|
||||||
|
application-label-ro:'Now in Android'
|
||||||
|
application-label-ru:'Now in Android'
|
||||||
|
application-label-si:'Now in Android'
|
||||||
|
application-label-sk:'Now in Android'
|
||||||
|
application-label-sl:'Now in Android'
|
||||||
|
application-label-sq:'Now in Android'
|
||||||
|
application-label-sr:'Now in Android'
|
||||||
|
application-label-sr-Latn:'Now in Android'
|
||||||
|
application-label-sv:'Now in Android'
|
||||||
|
application-label-sw:'Now in Android'
|
||||||
|
application-label-ta:'Now in Android'
|
||||||
|
application-label-te:'Now in Android'
|
||||||
|
application-label-th:'Now in Android'
|
||||||
|
application-label-tl:'Now in Android'
|
||||||
|
application-label-tr:'Now in Android'
|
||||||
|
application-label-uk:'Now in Android'
|
||||||
|
application-label-ur:'Now in Android'
|
||||||
|
application-label-uz:'Now in Android'
|
||||||
|
application-label-vi:'Now in Android'
|
||||||
|
application-label-zh-CN:'Now in Android'
|
||||||
|
application-label-zh-HK:'Now in Android'
|
||||||
|
application-label-zh-TW:'Now in Android'
|
||||||
|
application-label-zu:'Now in Android'
|
||||||
|
application-icon-120:'res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
application-icon-160:'res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
application-icon-240:'res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
application-icon-320:'res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
application-icon-480:'res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
|
||||||
|
launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon=''
|
||||||
|
uses-library-not-required:'android.ext.adservices'
|
||||||
|
uses-library-not-required:'androidx.window.extensions'
|
||||||
|
uses-library-not-required:'androidx.window.sidecar'
|
||||||
|
feature-group: label=''
|
||||||
|
uses-feature: name='android.hardware.faketouch'
|
||||||
|
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
|
||||||
|
main
|
||||||
|
other-activities
|
||||||
|
other-receivers
|
||||||
|
other-services
|
||||||
|
supports-screens: 'small' 'normal' 'large' 'xlarge'
|
||||||
|
supports-any-density: 'true'
|
||||||
|
locales: '--_--' 'af' 'am' 'ar' 'as' 'az' 'be' 'bg' 'bn' 'bs' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-CA' 'en-GB' 'en-IN' 'en-XC' 'es' 'es-US' 'et' 'eu' 'fa' 'fi' 'fr' 'fr-CA' 'gl' 'gu' 'hi' 'hr' 'hu' 'hy' 'in' 'is' 'it' 'iw' 'ja' 'ka' 'kk' 'km' 'kn' 'ko' 'ky' 'lo' 'lt' 'lv' 'mk' 'ml' 'mn' 'mr' 'ms' 'my' 'nb' 'ne' 'nl' 'or' 'pa' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'si' 'sk' 'sl' 'sq' 'sr' 'sr-Latn' 'sv' 'sw' 'ta' 'te' 'th' 'tl' 'tr' 'uk' 'ur' 'uz' 'vi' 'zh-CN' 'zh-HK' 'zh-TW' 'zu'
|
||||||
|
densities: '120' '160' '240' '320' '480' '640' '65534'
|
||||||
|
native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64'
|
||||||
@ -1,19 +0,0 @@
|
|||||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
|
||||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
|
||||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
|
||||||
-dontwarn org.conscrypt.Conscrypt$Version
|
|
||||||
-dontwarn org.conscrypt.Conscrypt
|
|
||||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
|
||||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
|
||||||
|
|
||||||
# Fix for Retrofit issue https://github.com/square/retrofit/issues/3751
|
|
||||||
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
|
||||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
|
||||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
|
||||||
|
|
||||||
# With R8 full mode generic signatures are stripped for classes that are not
|
|
||||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
|
||||||
# is used.
|
|
||||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
|
||||||
@ -1,268 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.samples.apps.nowinandroid.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
|
||||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.google.accompanist.testharness.TestHarness
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
|
||||||
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
|
|
||||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
|
||||||
import dagger.hilt.android.testing.BindValue
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.rules.TemporaryFolder
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests that the navigation UI is rendered correctly on different screen sizes.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
|
||||||
@HiltAndroidTest
|
|
||||||
class NavigationUiTest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the components' state and is used to perform injection on your test
|
|
||||||
*/
|
|
||||||
@get:Rule(order = 0)
|
|
||||||
val hiltRule = HiltAndroidRule(this)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a temporary folder used to create a Data Store file. This guarantees that
|
|
||||||
* the file is removed in between each test, preventing a crash.
|
|
||||||
*/
|
|
||||||
@BindValue
|
|
||||||
@get:Rule(order = 1)
|
|
||||||
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
|
|
||||||
*/
|
|
||||||
@get:Rule(order = 2)
|
|
||||||
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use a test activity to set the content on.
|
|
||||||
*/
|
|
||||||
@get:Rule(order = 3)
|
|
||||||
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
|
||||||
|
|
||||||
val userNewsResourceRepository = CompositeUserNewsResourceRepository(
|
|
||||||
newsRepository = TestNewsRepository(),
|
|
||||||
userDataRepository = TestUserDataRepository(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var networkMonitor: NetworkMonitor
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
hiltRule.inject()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun compactWidth_compactHeight_showsNavigationBar() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(400.dp, 400.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun mediumWidth_compactHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(610.dp, 400.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun expandedWidth_compactHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(900.dp, 400.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun compactWidth_mediumHeight_showsNavigationBar() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(400.dp, 500.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun mediumWidth_mediumHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(610.dp, 500.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun expandedWidth_mediumHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(900.dp, 500.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun compactWidth_expandedHeight_showsNavigationBar() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(400.dp, 1000.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun mediumWidth_expandedHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(610.dp, 1000.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun expandedWidth_expandedHeight_showsNavigationRail() {
|
|
||||||
composeTestRule.setContent {
|
|
||||||
TestHarness(size = DpSize(900.dp, 1000.dp)) {
|
|
||||||
BoxWithConstraints {
|
|
||||||
NiaApp(
|
|
||||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
|
||||||
DpSize(maxWidth, maxHeight),
|
|
||||||
),
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
|
||||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
|
||||||
|
fun AndroidComposeTestRule<*, *>.stringResource(
|
||||||
|
@StringRes resId: Int,
|
||||||
|
): ReadOnlyProperty<Any, String> =
|
||||||
|
ReadOnlyProperty { _, _ -> activity.getString(resId) }
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<!-- Allow users to distinguish between build variants by having a different background color
|
||||||
|
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
|
||||||
|
<color name="ic_launcher_background_tint">#FFFFFF</color>
|
||||||
|
<color name="ic_launcher_foreground_tint">#FF006780</color>
|
||||||
|
</resources>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<!-- Allow users to distinguish between build variants by having a different background color
|
||||||
|
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
|
||||||
|
<color name="ic_launcher_background_tint">#000000</color>
|
||||||
|
<color name="ic_launcher_foreground_tint">#FF006780</color>
|
||||||
|
</resources>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<!-- Allow users to distinguish between build variants by having a different background color
|
||||||
|
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
|
||||||
|
<color name="ic_launcher_background_tint">#000000</color>
|
||||||
|
<color name="ic_launcher_foreground_tint">#FFA23F16</color>
|
||||||
|
</resources>
|
||||||
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.samples.apps.nowinandroid.navigation
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import com.google.samples.apps.nowinandroid.R
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for the top level destinations in the application. Each of these destinations
|
|
||||||
* can contain one or more screens (based on the window size). Navigation from one screen to the
|
|
||||||
* next within a single destination will be handled directly in composables.
|
|
||||||
*/
|
|
||||||
enum class TopLevelDestination(
|
|
||||||
val selectedIcon: ImageVector,
|
|
||||||
val unselectedIcon: ImageVector,
|
|
||||||
val iconTextId: Int,
|
|
||||||
val titleTextId: Int,
|
|
||||||
) {
|
|
||||||
FOR_YOU(
|
|
||||||
selectedIcon = NiaIcons.Upcoming,
|
|
||||||
unselectedIcon = NiaIcons.UpcomingBorder,
|
|
||||||
iconTextId = forYouR.string.for_you,
|
|
||||||
titleTextId = R.string.app_name,
|
|
||||||
),
|
|
||||||
BOOKMARKS(
|
|
||||||
selectedIcon = NiaIcons.Bookmarks,
|
|
||||||
unselectedIcon = NiaIcons.BookmarksBorder,
|
|
||||||
iconTextId = bookmarksR.string.saved,
|
|
||||||
titleTextId = bookmarksR.string.saved,
|
|
||||||
),
|
|
||||||
INTERESTS(
|
|
||||||
selectedIcon = NiaIcons.Grid3x3,
|
|
||||||
unselectedIcon = NiaIcons.Grid3x3,
|
|
||||||
iconTextId = interestsR.string.interests,
|
|
||||||
titleTextId = interestsR.string.interests,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
@ -1,309 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.samples.apps.nowinandroid.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarDuration.Indefinite
|
|
||||||
import androidx.compose.material3.SnackbarDuration.Short
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult.ActionPerformed
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.composed
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.navigation.NavDestination
|
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
|
||||||
import com.google.samples.apps.nowinandroid.R
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRail
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRailItem
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
|
|
||||||
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
|
|
||||||
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
|
|
||||||
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
|
|
||||||
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
|
|
||||||
|
|
||||||
@OptIn(
|
|
||||||
ExperimentalMaterial3Api::class,
|
|
||||||
ExperimentalLayoutApi::class,
|
|
||||||
ExperimentalComposeUiApi::class,
|
|
||||||
)
|
|
||||||
@Composable
|
|
||||||
fun NiaApp(
|
|
||||||
windowSizeClass: WindowSizeClass,
|
|
||||||
networkMonitor: NetworkMonitor,
|
|
||||||
userNewsResourceRepository: UserNewsResourceRepository,
|
|
||||||
appState: NiaAppState = rememberNiaAppState(
|
|
||||||
networkMonitor = networkMonitor,
|
|
||||||
windowSizeClass = windowSizeClass,
|
|
||||||
userNewsResourceRepository = userNewsResourceRepository,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
val shouldShowGradientBackground =
|
|
||||||
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
|
|
||||||
var showSettingsDialog by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
NiaBackground {
|
|
||||||
NiaGradientBackground(
|
|
||||||
gradientColors = if (shouldShowGradientBackground) {
|
|
||||||
LocalGradientColors.current
|
|
||||||
} else {
|
|
||||||
GradientColors()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
// If user is not connected to the internet show a snack bar to inform them.
|
|
||||||
val notConnectedMessage = stringResource(R.string.not_connected)
|
|
||||||
LaunchedEffect(isOffline) {
|
|
||||||
if (isOffline) {
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = notConnectedMessage,
|
|
||||||
duration = Indefinite,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showSettingsDialog) {
|
|
||||||
SettingsDialog(
|
|
||||||
onDismiss = { showSettingsDialog = false },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
modifier = Modifier.semantics {
|
|
||||||
testTagsAsResourceId = true
|
|
||||||
},
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
||||||
bottomBar = {
|
|
||||||
if (appState.shouldShowBottomBar) {
|
|
||||||
NiaBottomBar(
|
|
||||||
destinations = appState.topLevelDestinations,
|
|
||||||
destinationsWithUnreadResources = unreadDestinations,
|
|
||||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
|
||||||
currentDestination = appState.currentDestination,
|
|
||||||
modifier = Modifier.testTag("NiaBottomBar"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { padding ->
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
.windowInsetsPadding(
|
|
||||||
WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Horizontal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
if (appState.shouldShowNavRail) {
|
|
||||||
NiaNavRail(
|
|
||||||
destinations = appState.topLevelDestinations,
|
|
||||||
destinationsWithUnreadResources = unreadDestinations,
|
|
||||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
|
||||||
currentDestination = appState.currentDestination,
|
|
||||||
modifier = Modifier
|
|
||||||
.testTag("NiaNavRail")
|
|
||||||
.safeDrawingPadding(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize()) {
|
|
||||||
// Show the top app bar on top level destinations.
|
|
||||||
val destination = appState.currentTopLevelDestination
|
|
||||||
if (destination != null) {
|
|
||||||
NiaTopAppBar(
|
|
||||||
titleRes = destination.titleTextId,
|
|
||||||
navigationIcon = NiaIcons.Search,
|
|
||||||
navigationIconContentDescription = stringResource(
|
|
||||||
id = settingsR.string.top_app_bar_navigation_icon_description,
|
|
||||||
),
|
|
||||||
actionIcon = NiaIcons.Settings,
|
|
||||||
actionIconContentDescription = stringResource(
|
|
||||||
id = settingsR.string.top_app_bar_action_icon_description,
|
|
||||||
),
|
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
onActionClick = { showSettingsDialog = true },
|
|
||||||
onNavigationClick = { appState.navigateToSearch() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NiaNavHost(appState = appState, onShowSnackbar = { message, action ->
|
|
||||||
snackbarHostState.showSnackbar(
|
|
||||||
message = message,
|
|
||||||
actionLabel = action,
|
|
||||||
duration = Short,
|
|
||||||
) == ActionPerformed
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: We may want to add padding or spacer when the snackbar is shown so that
|
|
||||||
// content doesn't display behind it.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NiaNavRail(
|
|
||||||
destinations: List<TopLevelDestination>,
|
|
||||||
destinationsWithUnreadResources: Set<TopLevelDestination>,
|
|
||||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
|
||||||
currentDestination: NavDestination?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
NiaNavigationRail(modifier = modifier) {
|
|
||||||
destinations.forEach { destination ->
|
|
||||||
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
|
|
||||||
val hasUnread = destinationsWithUnreadResources.contains(destination)
|
|
||||||
NiaNavigationRailItem(
|
|
||||||
selected = selected,
|
|
||||||
onClick = { onNavigateToDestination(destination) },
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = destination.unselectedIcon,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
selectedIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = destination.selectedIcon,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(destination.iconTextId)) },
|
|
||||||
modifier = if (hasUnread) Modifier.notificationDot() else Modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NiaBottomBar(
|
|
||||||
destinations: List<TopLevelDestination>,
|
|
||||||
destinationsWithUnreadResources: Set<TopLevelDestination>,
|
|
||||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
|
||||||
currentDestination: NavDestination?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
NiaNavigationBar(
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
destinations.forEach { destination ->
|
|
||||||
val hasUnread = destinationsWithUnreadResources.contains(destination)
|
|
||||||
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
|
|
||||||
NiaNavigationBarItem(
|
|
||||||
selected = selected,
|
|
||||||
onClick = { onNavigateToDestination(destination) },
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = destination.unselectedIcon,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
selectedIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = destination.selectedIcon,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(destination.iconTextId)) },
|
|
||||||
modifier = if (hasUnread) Modifier.notificationDot() else Modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Modifier.notificationDot(): Modifier =
|
|
||||||
composed {
|
|
||||||
val tertiaryColor = MaterialTheme.colorScheme.tertiary
|
|
||||||
drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawCircle(
|
|
||||||
tertiaryColor,
|
|
||||||
radius = 5.dp.toPx(),
|
|
||||||
// This is based on the dimensions of the NavigationBar's "indicator pill";
|
|
||||||
// however, its parameters are private, so we must depend on them implicitly
|
|
||||||
// (NavigationBarTokens.ActiveIndicatorWidth = 64.dp)
|
|
||||||
center = center + Offset(
|
|
||||||
64.dp.toPx() * .45f,
|
|
||||||
32.dp.toPx() * -.45f - 6.dp.toPx(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
|
|
||||||
this?.hierarchy?.any {
|
|
||||||
it.route?.contains(destination.name, true) ?: false
|
|
||||||
} ?: false
|
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.navigation
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import com.google.samples.apps.nowinandroid.R
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.search.R as searchR
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for the top level destinations in the application. Contains metadata about the destination
|
||||||
|
* that is used in the top app bar and common navigation UI.
|
||||||
|
*
|
||||||
|
* @param selectedIcon The icon to be displayed in the navigation UI when this destination is
|
||||||
|
* selected.
|
||||||
|
* @param unselectedIcon The icon to be displayed in the navigation UI when this destination is
|
||||||
|
* not selected.
|
||||||
|
* @param iconTextId Text that to be displayed in the navigation UI.
|
||||||
|
* @param titleTextId Text that is displayed on the top app bar.
|
||||||
|
* @param route The route to use when navigating to this destination.
|
||||||
|
* @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that
|
||||||
|
* there is a single destination in that section of the app (no nested destinations).
|
||||||
|
*/
|
||||||
|
enum class TopLevelDestination(
|
||||||
|
val selectedIcon: ImageVector,
|
||||||
|
val unselectedIcon: ImageVector,
|
||||||
|
@StringRes val iconTextId: Int,
|
||||||
|
@StringRes val titleTextId: Int,
|
||||||
|
val route: KClass<*>,
|
||||||
|
val baseRoute: KClass<*> = route,
|
||||||
|
) {
|
||||||
|
FOR_YOU(
|
||||||
|
selectedIcon = NiaIcons.Upcoming,
|
||||||
|
unselectedIcon = NiaIcons.UpcomingBorder,
|
||||||
|
iconTextId = forYouR.string.feature_foryou_title,
|
||||||
|
titleTextId = R.string.app_name,
|
||||||
|
route = ForYouRoute::class,
|
||||||
|
baseRoute = ForYouBaseRoute::class,
|
||||||
|
),
|
||||||
|
BOOKMARKS(
|
||||||
|
selectedIcon = NiaIcons.Bookmarks,
|
||||||
|
unselectedIcon = NiaIcons.BookmarksBorder,
|
||||||
|
iconTextId = bookmarksR.string.feature_bookmarks_title,
|
||||||
|
titleTextId = bookmarksR.string.feature_bookmarks_title,
|
||||||
|
route = BookmarksRoute::class,
|
||||||
|
),
|
||||||
|
INTERESTS(
|
||||||
|
selectedIcon = NiaIcons.Grid3x3,
|
||||||
|
unselectedIcon = NiaIcons.Grid3x3,
|
||||||
|
iconTextId = searchR.string.feature_search_interests,
|
||||||
|
titleTextId = searchR.string.feature_search_interests,
|
||||||
|
route = InterestsRoute::class,
|
||||||
|
),
|
||||||
|
}
|
||||||
@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||||
|
import androidx.compose.material3.SnackbarDuration.Short
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult.ActionPerformed
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||||
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavDestination
|
||||||
|
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||||
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
|
import com.google.samples.apps.nowinandroid.R
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationSuiteScaffold
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
|
||||||
|
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
|
||||||
|
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun NiaApp(
|
||||||
|
appState: NiaAppState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
|
||||||
|
) {
|
||||||
|
val shouldShowGradientBackground =
|
||||||
|
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
|
||||||
|
var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
NiaBackground(modifier = modifier) {
|
||||||
|
NiaGradientBackground(
|
||||||
|
gradientColors = if (shouldShowGradientBackground) {
|
||||||
|
LocalGradientColors.current
|
||||||
|
} else {
|
||||||
|
GradientColors()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
// If user is not connected to the internet show a snack bar to inform them.
|
||||||
|
val notConnectedMessage = stringResource(R.string.not_connected)
|
||||||
|
LaunchedEffect(isOffline) {
|
||||||
|
if (isOffline) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = notConnectedMessage,
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NiaApp(
|
||||||
|
appState = appState,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
showSettingsDialog = showSettingsDialog,
|
||||||
|
onSettingsDismissed = { showSettingsDialog = false },
|
||||||
|
onTopAppBarActionClick = { showSettingsDialog = true },
|
||||||
|
windowAdaptiveInfo = windowAdaptiveInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(
|
||||||
|
ExperimentalMaterial3Api::class,
|
||||||
|
ExperimentalComposeUiApi::class,
|
||||||
|
ExperimentalMaterial3AdaptiveApi::class,
|
||||||
|
)
|
||||||
|
internal fun NiaApp(
|
||||||
|
appState: NiaAppState,
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
showSettingsDialog: Boolean,
|
||||||
|
onSettingsDismissed: () -> Unit,
|
||||||
|
onTopAppBarActionClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
|
||||||
|
) {
|
||||||
|
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources
|
||||||
|
.collectAsStateWithLifecycle()
|
||||||
|
val currentDestination = appState.currentDestination
|
||||||
|
|
||||||
|
if (showSettingsDialog) {
|
||||||
|
SettingsDialog(
|
||||||
|
onDismiss = { onSettingsDismissed() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NiaNavigationSuiteScaffold(
|
||||||
|
navigationSuiteItems = {
|
||||||
|
appState.topLevelDestinations.forEach { destination ->
|
||||||
|
val hasUnread = unreadDestinations.contains(destination)
|
||||||
|
val selected = currentDestination
|
||||||
|
.isRouteInHierarchy(destination.baseRoute)
|
||||||
|
item(
|
||||||
|
selected = selected,
|
||||||
|
onClick = { appState.navigateToTopLevelDestination(destination) },
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = destination.unselectedIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selectedIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = destination.selectedIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(destination.iconTextId)) },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.testTag("NiaNavItem")
|
||||||
|
.then(if (hasUnread) Modifier.notificationDot() else Modifier),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowAdaptiveInfo = windowAdaptiveInfo,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier.semantics {
|
||||||
|
testTagsAsResourceId = true
|
||||||
|
},
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Horizontal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
// Show the top app bar on top level destinations.
|
||||||
|
val destination = appState.currentTopLevelDestination
|
||||||
|
var shouldShowTopAppBar = false
|
||||||
|
|
||||||
|
if (destination != null) {
|
||||||
|
shouldShowTopAppBar = true
|
||||||
|
NiaTopAppBar(
|
||||||
|
titleRes = destination.titleTextId,
|
||||||
|
navigationIcon = NiaIcons.Search,
|
||||||
|
navigationIconContentDescription = stringResource(
|
||||||
|
id = settingsR.string.feature_settings_top_app_bar_navigation_icon_description,
|
||||||
|
),
|
||||||
|
actionIcon = NiaIcons.Settings,
|
||||||
|
actionIconContentDescription = stringResource(
|
||||||
|
id = settingsR.string.feature_settings_top_app_bar_action_icon_description,
|
||||||
|
),
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
onActionClick = { onTopAppBarActionClick() },
|
||||||
|
onNavigationClick = { appState.navigateToSearch() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
// Workaround for https://issuetracker.google.com/338478720
|
||||||
|
modifier = Modifier.consumeWindowInsets(
|
||||||
|
if (shouldShowTopAppBar) {
|
||||||
|
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
|
||||||
|
} else {
|
||||||
|
WindowInsets(0, 0, 0, 0)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
NiaNavHost(
|
||||||
|
appState = appState,
|
||||||
|
onShowSnackbar = { message, action ->
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = message,
|
||||||
|
actionLabel = action,
|
||||||
|
duration = Short,
|
||||||
|
) == ActionPerformed
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We may want to add padding or spacer when the snackbar is shown so that
|
||||||
|
// content doesn't display behind it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.notificationDot(): Modifier =
|
||||||
|
composed {
|
||||||
|
val tertiaryColor = MaterialTheme.colorScheme.tertiary
|
||||||
|
drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawCircle(
|
||||||
|
tertiaryColor,
|
||||||
|
radius = 5.dp.toPx(),
|
||||||
|
// This is based on the dimensions of the NavigationBar's "indicator pill";
|
||||||
|
// however, its parameters are private, so we must depend on them implicitly
|
||||||
|
// (NavigationBarTokens.ActiveIndicatorWidth = 64.dp)
|
||||||
|
center = center + Offset(
|
||||||
|
64.dp.toPx() * .45f,
|
||||||
|
32.dp.toPx() * -.45f - 6.dp.toPx(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
|
||||||
|
this?.hierarchy?.any {
|
||||||
|
it.hasRoute(route)
|
||||||
|
} ?: false
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui.interests2pane
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
const val TOPIC_ID_KEY = "selectedTopicId"
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class Interests2PaneViewModel @Inject constructor(
|
||||||
|
private val savedStateHandle: SavedStateHandle,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val route = savedStateHandle.toRoute<InterestsRoute>()
|
||||||
|
val selectedTopicId: StateFlow<String?> = savedStateHandle.getStateFlow(
|
||||||
|
key = TOPIC_ID_KEY,
|
||||||
|
initialValue = route.initialTopicId,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onTopicClick(topicId: String?) {
|
||||||
|
savedStateHandle[TOPIC_ID_KEY] = topicId
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui.interests2pane
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||||
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
|
import androidx.compose.material3.adaptive.layout.AnimatedPane
|
||||||
|
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
|
||||||
|
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
|
||||||
|
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
||||||
|
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
|
||||||
|
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
|
||||||
|
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||||
|
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicRoute
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable internal object TopicPlaceholderRoute
|
||||||
|
|
||||||
|
// TODO: Remove @Keep when https://issuetracker.google.com/353898971 is fixed
|
||||||
|
@Keep
|
||||||
|
@Serializable internal object DetailPaneNavHostRoute
|
||||||
|
|
||||||
|
fun NavGraphBuilder.interestsListDetailScreen() {
|
||||||
|
composable<InterestsRoute> {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun InterestsListDetailScreen(
|
||||||
|
viewModel: Interests2PaneViewModel = hiltViewModel(),
|
||||||
|
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
|
||||||
|
) {
|
||||||
|
val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()
|
||||||
|
InterestsListDetailScreen(
|
||||||
|
selectedTopicId = selectedTopicId,
|
||||||
|
onTopicClick = viewModel::onTopicClick,
|
||||||
|
windowAdaptiveInfo = windowAdaptiveInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun InterestsListDetailScreen(
|
||||||
|
selectedTopicId: String?,
|
||||||
|
onTopicClick: (String) -> Unit,
|
||||||
|
windowAdaptiveInfo: WindowAdaptiveInfo,
|
||||||
|
) {
|
||||||
|
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator(
|
||||||
|
scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo),
|
||||||
|
initialDestinationHistory = listOfNotNull(
|
||||||
|
ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List),
|
||||||
|
ThreePaneScaffoldDestinationItem<Nothing>(ListDetailPaneScaffoldRole.Detail).takeIf {
|
||||||
|
selectedTopicId != null
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
BackHandler(listDetailNavigator.canNavigateBack()) {
|
||||||
|
listDetailNavigator.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestedNavHostStartRoute by remember {
|
||||||
|
val route = selectedTopicId?.let { TopicRoute(id = it) } ?: TopicPlaceholderRoute
|
||||||
|
mutableStateOf(route)
|
||||||
|
}
|
||||||
|
var nestedNavKey by rememberSaveable(
|
||||||
|
stateSaver = Saver({ it.toString() }, UUID::fromString),
|
||||||
|
) {
|
||||||
|
mutableStateOf(UUID.randomUUID())
|
||||||
|
}
|
||||||
|
val nestedNavController = key(nestedNavKey) {
|
||||||
|
rememberNavController()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTopicClickShowDetailPane(topicId: String) {
|
||||||
|
onTopicClick(topicId)
|
||||||
|
if (listDetailNavigator.isDetailPaneVisible()) {
|
||||||
|
// If the detail pane was visible, then use the nestedNavController navigate call
|
||||||
|
// directly
|
||||||
|
nestedNavController.navigateToTopic(topicId) {
|
||||||
|
popUpTo<DetailPaneNavHostRoute>()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, recreate the NavHost entirely, and start at the new destination
|
||||||
|
nestedNavHostStartRoute = TopicRoute(id = topicId)
|
||||||
|
nestedNavKey = UUID.randomUUID()
|
||||||
|
}
|
||||||
|
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
ListDetailPaneScaffold(
|
||||||
|
value = listDetailNavigator.scaffoldValue,
|
||||||
|
directive = listDetailNavigator.scaffoldDirective,
|
||||||
|
listPane = {
|
||||||
|
AnimatedPane {
|
||||||
|
InterestsRoute(
|
||||||
|
onTopicClick = ::onTopicClickShowDetailPane,
|
||||||
|
highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
detailPane = {
|
||||||
|
AnimatedPane {
|
||||||
|
key(nestedNavKey) {
|
||||||
|
NavHost(
|
||||||
|
navController = nestedNavController,
|
||||||
|
startDestination = nestedNavHostStartRoute,
|
||||||
|
route = DetailPaneNavHostRoute::class,
|
||||||
|
) {
|
||||||
|
topicScreen(
|
||||||
|
showBackButton = !listDetailNavigator.isListPaneVisible(),
|
||||||
|
onBackClick = listDetailNavigator::navigateBack,
|
||||||
|
onTopicClick = ::onTopicClickShowDetailPane,
|
||||||
|
)
|
||||||
|
composable<TopicPlaceholderRoute> {
|
||||||
|
TopicDetailPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
|
private fun <T> ThreePaneScaffoldNavigator<T>.isListPaneVisible(): Boolean =
|
||||||
|
scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
|
private fun <T> ThreePaneScaffoldNavigator<T>.isDetailPaneVisible(): Boolean =
|
||||||
|
scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.profileinstaller.ProfileVerifier
|
||||||
|
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.guava.await
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the app's Baseline Profile Compilation Status using [ProfileVerifier].
|
||||||
|
*
|
||||||
|
* When delivering through Google Play, the baseline profile is compiled during installation.
|
||||||
|
* In this case you will see the correct state logged without any further action necessary.
|
||||||
|
* To verify baseline profile installation locally, you need to manually trigger baseline
|
||||||
|
* profile installation.
|
||||||
|
*
|
||||||
|
* For immediate compilation, call:
|
||||||
|
* ```bash
|
||||||
|
* adb shell cmd package compile -f -m speed-profile com.example.macrobenchmark.target
|
||||||
|
* ```
|
||||||
|
* You can also trigger background optimizations:
|
||||||
|
* ```bash
|
||||||
|
* adb shell pm bg-dexopt-job
|
||||||
|
* ```
|
||||||
|
* Both jobs run asynchronously and might take some time complete.
|
||||||
|
*
|
||||||
|
* To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`.
|
||||||
|
* If you don't do either of these steps, you might only see the profile status reported as
|
||||||
|
* "enqueued for compilation" when running the sample locally.
|
||||||
|
*
|
||||||
|
* @see androidx.profileinstaller.ProfileVerifier.CompilationStatus.ResultCode
|
||||||
|
*/
|
||||||
|
class ProfileVerifierLogger @Inject constructor(
|
||||||
|
@ApplicationScope private val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "ProfileInstaller"
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun invoke() = scope.launch {
|
||||||
|
val status = ProfileVerifier.getCompilationStatusAsync().await()
|
||||||
|
Log.d(TAG, "Status code: ${status.profileInstallResultCode}")
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
when {
|
||||||
|
status.isCompiledWithProfile -> "App compiled with profile"
|
||||||
|
status.hasProfileEnqueuedForCompilation() -> "Profile enqueued for compilation"
|
||||||
|
else -> "Profile not compiled nor enqueued"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.platform.AbstractComposeView
|
||||||
|
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.children
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [DeviceConfigurationOverride] that overrides the window insets for the contained content.
|
||||||
|
*/
|
||||||
|
@Suppress("ktlint:standard:function-naming")
|
||||||
|
fun DeviceConfigurationOverride.Companion.WindowInsets(
|
||||||
|
windowInsets: WindowInsetsCompat,
|
||||||
|
): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
|
||||||
|
val currentContentUnderTest by rememberUpdatedState(contentUnderTest)
|
||||||
|
val currentWindowInsets by rememberUpdatedState(windowInsets)
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
object : AbstractComposeView(context) {
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
currentContentUnderTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
children.forEach {
|
||||||
|
it.dispatchApplyWindowInsets(
|
||||||
|
WindowInsets(currentWindowInsets.toWindowInsets()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return WindowInsetsCompat.CONSUMED.toWindowInsets()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated, but intercept the `requestApplyInsets` call via the deprecated
|
||||||
|
* method.
|
||||||
|
*/
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun requestFitSystemWindows() {
|
||||||
|
dispatchApplyWindowInsets(WindowInsets(currentWindowInsets.toWindowInsets()!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { with(currentWindowInsets) { it.requestApplyInsets() } },
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.Topic
|
||||||
|
import com.google.samples.apps.nowinandroid.ui.interests2pane.InterestsListDetailScreen
|
||||||
|
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.topic.R as FeatureTopicR
|
||||||
|
|
||||||
|
private const val EXPANDED_WIDTH = "w1200dp-h840dp"
|
||||||
|
private const val COMPACT_WIDTH = "w412dp-h915dp"
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(application = HiltTestApplication::class)
|
||||||
|
class InterestsListDetailScreenTest {
|
||||||
|
|
||||||
|
@get:Rule(order = 0)
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var topicsRepository: TopicsRepository
|
||||||
|
|
||||||
|
/** Convenience function for getting all topics during tests, */
|
||||||
|
private fun getTopics(): List<Topic> = runBlocking {
|
||||||
|
topicsRepository.getTopics().first().sortedBy { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The strings used for matching in these tests.
|
||||||
|
private val placeholderText by composeTestRule.stringResource(FeatureTopicR.string.feature_topic_select_an_interest)
|
||||||
|
private val listPaneTag = "interests:topics"
|
||||||
|
|
||||||
|
private val Topic.testTag
|
||||||
|
get() = "topic:${this.id}"
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
hiltRule.inject()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = EXPANDED_WIDTH)
|
||||||
|
fun expandedWidth_initialState_showsTwoPanesWithPlaceholder() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = COMPACT_WIDTH)
|
||||||
|
fun compactWidth_initialState_showsListPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = EXPANDED_WIDTH)
|
||||||
|
fun expandedWidth_topicSelected_updatesDetailPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
onNodeWithTag(firstTopic.testTag).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = COMPACT_WIDTH)
|
||||||
|
fun compactWidth_topicSelected_showsTopicDetailPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsNotDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
onNodeWithTag(firstTopic.testTag).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = EXPANDED_WIDTH)
|
||||||
|
fun expandedWidth_backPressFromTopicDetail_leavesInterests() {
|
||||||
|
var unhandledBackPress = false
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
// Back press should not be handled by the two pane layout, and thus
|
||||||
|
// "fall through" to this BackHandler.
|
||||||
|
BackHandler {
|
||||||
|
unhandledBackPress = true
|
||||||
|
}
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
waitForIdle()
|
||||||
|
Espresso.pressBack()
|
||||||
|
|
||||||
|
assertTrue(unhandledBackPress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(qualifiers = COMPACT_WIDTH)
|
||||||
|
fun compactWidth_backPressFromTopicDetail_showsListPane() {
|
||||||
|
composeTestRule.apply {
|
||||||
|
setContent {
|
||||||
|
NiaTheme {
|
||||||
|
InterestsListDetailScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstTopic = getTopics().first()
|
||||||
|
onNodeWithText(firstTopic.name).performClick()
|
||||||
|
|
||||||
|
waitForIdle()
|
||||||
|
Espresso.pressBack()
|
||||||
|
|
||||||
|
onNodeWithTag(listPaneTag).assertIsDisplayed()
|
||||||
|
onNodeWithText(placeholderText).assertIsNotDisplayed()
|
||||||
|
onNodeWithTag(firstTopic.testTag).assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AndroidComposeTestRule<*, *>.stringResource(
|
||||||
|
@StringRes resId: Int,
|
||||||
|
): ReadOnlyProperty<Any, String> =
|
||||||
|
ReadOnlyProperty { _, _ -> activity.getString(resId) }
|
||||||
@ -0,0 +1,339 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsEndWidth
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsStartWidth
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||||
|
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||||
|
import androidx.compose.material3.adaptive.Posture
|
||||||
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toAndroidRect
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||||
|
import androidx.compose.ui.test.ForcedSize
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpRect
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.roundToIntRect
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
|
||||||
|
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
import org.robolectric.annotation.LooperMode
|
||||||
|
import java.util.TimeZone
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the Snackbar is correctly displayed on different screen sizes.
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
|
||||||
|
// This allows enough room to render the content under test without clipping or scaling.
|
||||||
|
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi")
|
||||||
|
@LooperMode(LooperMode.Mode.PAUSED)
|
||||||
|
@HiltAndroidTest
|
||||||
|
class SnackbarInsetsScreenshotTests {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the components' state and is used to perform injection on your test
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 0)
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a test activity to set the content on.
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userDataRepository: FakeUserDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var topicsRepository: TopicsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userNewsResourceRepository: UserNewsResourceRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
hiltRule.inject()
|
||||||
|
|
||||||
|
// Configure user data
|
||||||
|
runBlocking {
|
||||||
|
userDataRepository.setShouldHideOnboarding(true)
|
||||||
|
|
||||||
|
userDataRepository.setFollowedTopicIds(
|
||||||
|
setOf(topicsRepository.getTopics().first().first().id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setTimeZone() {
|
||||||
|
// Make time zone deterministic in tests
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun phone_noSnackbar() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"insets_snackbar_compact_medium_noSnackbar",
|
||||||
|
action = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_phone() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"insets_snackbar_compact_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_foldable() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
600.dp,
|
||||||
|
600.dp,
|
||||||
|
"insets_snackbar_medium_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_tablet() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
900.dp,
|
||||||
|
900.dp,
|
||||||
|
"insets_snackbar_expanded_expanded",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
|
private fun testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
width: Dp,
|
||||||
|
height: Dp,
|
||||||
|
screenshotName: String,
|
||||||
|
action: suspend () -> Unit,
|
||||||
|
) {
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
composeTestRule.setContent {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// Replaces images with placeholders
|
||||||
|
LocalInspectionMode provides true,
|
||||||
|
) {
|
||||||
|
scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
DeviceConfigurationOverride(
|
||||||
|
DeviceConfigurationOverride.ForcedSize(DpSize(width, height)),
|
||||||
|
) {
|
||||||
|
DeviceConfigurationOverride(
|
||||||
|
DeviceConfigurationOverride.WindowInsets(
|
||||||
|
WindowInsetsCompat.Builder()
|
||||||
|
.setInsets(
|
||||||
|
WindowInsetsCompat.Type.statusBars(),
|
||||||
|
DpRect(
|
||||||
|
left = 0.dp,
|
||||||
|
top = 64.dp,
|
||||||
|
right = 0.dp,
|
||||||
|
bottom = 0.dp,
|
||||||
|
).toInsets(),
|
||||||
|
)
|
||||||
|
.setInsets(
|
||||||
|
WindowInsetsCompat.Type.navigationBars(),
|
||||||
|
DpRect(
|
||||||
|
left = 64.dp,
|
||||||
|
top = 0.dp,
|
||||||
|
right = 64.dp,
|
||||||
|
bottom = 64.dp,
|
||||||
|
).toInsets(),
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
BoxWithConstraints(Modifier.testTag("root")) {
|
||||||
|
NiaTheme {
|
||||||
|
val appState = rememberNiaAppState(
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
userNewsResourceRepository = userNewsResourceRepository,
|
||||||
|
timeZoneMonitor = timeZoneMonitor,
|
||||||
|
)
|
||||||
|
NiaApp(
|
||||||
|
appState = appState,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
showSettingsDialog = false,
|
||||||
|
onSettingsDismissed = {},
|
||||||
|
onTopAppBarActionClick = {},
|
||||||
|
windowAdaptiveInfo = WindowAdaptiveInfo(
|
||||||
|
windowSizeClass = WindowSizeClass.compute(
|
||||||
|
maxWidth.value,
|
||||||
|
maxHeight.value,
|
||||||
|
),
|
||||||
|
windowPosture = Posture(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DebugVisibleWindowInsets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("root")
|
||||||
|
.captureRoboImage(
|
||||||
|
"src/testDemo/screenshots/$screenshotName.png",
|
||||||
|
roborazziOptions = DefaultRoborazziOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DebugVisibleWindowInsets(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
debugColor: Color = Color.Magenta.copy(alpha = 0.5f),
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.windowInsetsStartWidth(WindowInsets.safeDrawing)
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical))
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.windowInsetsEndWidth(WindowInsets.safeDrawing)
|
||||||
|
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical))
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsTopHeight(WindowInsets.safeDrawing)
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.windowInsetsBottomHeight(WindowInsets.safeDrawing)
|
||||||
|
.background(debugColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DpRect.toInsets() = toInsets(LocalDensity.current)
|
||||||
|
|
||||||
|
private fun DpRect.toInsets(density: Density) =
|
||||||
|
Insets.of(with(density) { toRect() }.roundToIntRect().toAndroidRect())
|
||||||
@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||||
|
import androidx.compose.material3.adaptive.Posture
|
||||||
|
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||||
|
import androidx.compose.ui.test.ForcedSize
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
|
||||||
|
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
import org.robolectric.annotation.LooperMode
|
||||||
|
import java.util.TimeZone
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the Snackbar is correctly displayed on different screen sizes.
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
|
||||||
|
// This allows enough room to render the content under test without clipping or scaling.
|
||||||
|
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi")
|
||||||
|
@LooperMode(LooperMode.Mode.PAUSED)
|
||||||
|
@HiltAndroidTest
|
||||||
|
class SnackbarScreenshotTests {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the components' state and is used to perform injection on your test
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 0)
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a test activity to set the content on.
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userDataRepository: FakeUserDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var topicsRepository: TopicsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userNewsResourceRepository: UserNewsResourceRepository
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
hiltRule.inject()
|
||||||
|
|
||||||
|
// Configure user data
|
||||||
|
runBlocking {
|
||||||
|
userDataRepository.setShouldHideOnboarding(true)
|
||||||
|
|
||||||
|
userDataRepository.setFollowedTopicIds(
|
||||||
|
setOf(topicsRepository.getTopics().first().first().id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setTimeZone() {
|
||||||
|
// Make time zone deterministic in tests
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun phone_noSnackbar() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"snackbar_compact_medium_noSnackbar",
|
||||||
|
action = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_phone() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
400.dp,
|
||||||
|
500.dp,
|
||||||
|
"snackbar_compact_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_foldable() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
600.dp,
|
||||||
|
600.dp,
|
||||||
|
"snackbar_medium_medium",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snackbarShown_tablet() {
|
||||||
|
val snackbarHostState = SnackbarHostState()
|
||||||
|
testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState,
|
||||||
|
900.dp,
|
||||||
|
900.dp,
|
||||||
|
"snackbar_expanded_expanded",
|
||||||
|
) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
"This is a test snackbar message",
|
||||||
|
actionLabel = "Action Label",
|
||||||
|
duration = Indefinite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
|
private fun testSnackbarScreenshotWithSize(
|
||||||
|
snackbarHostState: SnackbarHostState,
|
||||||
|
width: Dp,
|
||||||
|
height: Dp,
|
||||||
|
screenshotName: String,
|
||||||
|
action: suspend () -> Unit,
|
||||||
|
) {
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
composeTestRule.setContent {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
// Replaces images with placeholders
|
||||||
|
LocalInspectionMode provides true,
|
||||||
|
) {
|
||||||
|
scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
DeviceConfigurationOverride(
|
||||||
|
DeviceConfigurationOverride.ForcedSize(DpSize(width, height)),
|
||||||
|
) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaTheme {
|
||||||
|
val appState = rememberNiaAppState(
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
userNewsResourceRepository = userNewsResourceRepository,
|
||||||
|
timeZoneMonitor = timeZoneMonitor,
|
||||||
|
)
|
||||||
|
NiaApp(
|
||||||
|
appState = appState,
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
showSettingsDialog = false,
|
||||||
|
onSettingsDismissed = {},
|
||||||
|
onTopAppBarActionClick = {},
|
||||||
|
windowAdaptiveInfo = WindowAdaptiveInfo(
|
||||||
|
windowSizeClass = WindowSizeClass.compute(
|
||||||
|
maxWidth.value,
|
||||||
|
maxHeight.value,
|
||||||
|
),
|
||||||
|
windowPosture = Posture(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onRoot()
|
||||||
|
.captureRoboImage(
|
||||||
|
"src/testDemo/screenshots/$screenshotName.png",
|
||||||
|
roborazziOptions = DefaultRoborazziOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 93 KiB |
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid
|
||||||
|
|
||||||
|
import androidx.benchmark.macro.ExperimentalMetricApi
|
||||||
|
import androidx.benchmark.macro.StartupTimingMetric
|
||||||
|
import androidx.benchmark.macro.TraceSectionMetric
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Metrics to measure baseline profile effectiveness.
|
||||||
|
*/
|
||||||
|
class BaselineProfileMetrics {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* A [TraceSectionMetric] that tracks the time spent in JIT compilation.
|
||||||
|
*
|
||||||
|
* This number should go down when a baseline profile is applied properly.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMetricApi::class)
|
||||||
|
val jitCompilationMetric = TraceSectionMetric("JIT Compiling %", label = "JIT compilation")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TraceSectionMetric] that tracks the time spent in class initialization.
|
||||||
|
*
|
||||||
|
* This number should go down when a baseline profile is applied properly.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMetricApi::class)
|
||||||
|
val classInitMetric = TraceSectionMetric("L%/%;", label = "ClassInit")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metrics relevant to startup and baseline profile effectiveness measurement.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMetricApi::class)
|
||||||
|
val allMetrics = listOf(StartupTimingMetric(), jitCompilationMetric, classInitMetric)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.baselineprofile
|
||||||
|
|
||||||
|
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||||
|
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||||
|
import com.google.samples.apps.nowinandroid.bookmarks.goToBookmarksScreen
|
||||||
|
import com.google.samples.apps.nowinandroid.startActivityAndAllowNotifications
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baseline Profile of the "Bookmarks" screen
|
||||||
|
*/
|
||||||
|
class BookmarksBaselineProfile {
|
||||||
|
@get:Rule val baselineProfileRule = BaselineProfileRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun generate() =
|
||||||
|
baselineProfileRule.collect(PACKAGE_NAME) {
|
||||||
|
startActivityAndAllowNotifications()
|
||||||
|
|
||||||
|
// Navigate to saved screen
|
||||||
|
goToBookmarksScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.baselineprofile
|
||||||
|
|
||||||
|
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||||
|
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||||
|
import com.google.samples.apps.nowinandroid.interests.goToInterestsScreen
|
||||||
|
import com.google.samples.apps.nowinandroid.interests.interestsScrollTopicsDownUp
|
||||||
|
import com.google.samples.apps.nowinandroid.startActivityAndAllowNotifications
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baseline Profile of the "Interests" screen
|
||||||
|
*/
|
||||||
|
class InterestsBaselineProfile {
|
||||||
|
@get:Rule val baselineProfileRule = BaselineProfileRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun generate() =
|
||||||
|
baselineProfileRule.collect(PACKAGE_NAME) {
|
||||||
|
startActivityAndAllowNotifications()
|
||||||
|
|
||||||
|
// Navigate to interests screen
|
||||||
|
goToInterestsScreen()
|
||||||
|
interestsScrollTopicsDownUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.samples.apps.nowinandroid.baselineprofile
|
||||||
|
|
||||||
|
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||||
|
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||||
|
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||||
|
import com.google.samples.apps.nowinandroid.startActivityAndAllowNotifications
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baseline Profile for app startup. This profile also enables using [Dex Layout Optimizations](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations)
|
||||||
|
* via the `includeInStartupProfile` parameter.
|
||||||
|
*/
|
||||||
|
class StartupBaselineProfile {
|
||||||
|
@get:Rule val baselineProfileRule = BaselineProfileRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun generate() = baselineProfileRule.collect(
|
||||||
|
PACKAGE_NAME,
|
||||||
|
includeInStartupProfile = true,
|
||||||
|
profileBlock = MacrobenchmarkScope::startActivityAndAllowNotifications,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 com.google.samples.apps.nowinandroid.libs
|
|
||||||
import org.gradle.api.Plugin
|
|
||||||
import org.gradle.api.Project
|
|
||||||
import org.gradle.kotlin.dsl.dependencies
|
|
||||||
|
|
||||||
class AndroidHiltConventionPlugin : Plugin<Project> {
|
|
||||||
override fun apply(target: Project) {
|
|
||||||
with(target) {
|
|
||||||
with(pluginManager) {
|
|
||||||
apply("dagger.hilt.android.plugin")
|
|
||||||
// KAPT must go last to avoid build warnings.
|
|
||||||
// See: https://stackoverflow.com/questions/70550883/warning-the-following-options-were-not-recognized-by-any-processor-dagger-f
|
|
||||||
apply("org.jetbrains.kotlin.kapt")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
"implementation"(libs.findLibrary("hilt.android").get())
|
|
||||||
"kapt"(libs.findLibrary("hilt.compiler").get())
|
|
||||||
"kaptAndroidTest"(libs.findLibrary("hilt.compiler").get())
|
|
||||||
"kaptTest"(libs.findLibrary("hilt.compiler").get())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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.android.build.gradle.api.AndroidBasePlugin
|
||||||
|
import com.google.samples.apps.nowinandroid.libs
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
|
||||||
|
class HiltConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
pluginManager.apply("com.google.devtools.ksp")
|
||||||
|
dependencies {
|
||||||
|
add("ksp", libs.findLibrary("hilt.compiler").get())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add support for Jvm Module, base on org.jetbrains.kotlin.jvm
|
||||||
|
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
|
||||||
|
dependencies {
|
||||||
|
add("implementation", libs.findLibrary("hilt.core").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add support for Android modules, based on [AndroidBasePlugin] */
|
||||||
|
pluginManager.withPlugin("com.android.base") {
|
||||||
|
pluginManager.apply("dagger.hilt.android.plugin")
|
||||||
|
dependencies {
|
||||||
|
add("implementation", libs.findLibrary("hilt.android").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||