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

|

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

|

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