@ -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
|
||||
3
|
||||
2
|
||||
|
@ -1,3 +1,3 @@
|
||||
# :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
|
||||
|
||||

|
||||
## 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|