Showing preview only (288K chars total). Download the full file or copy to clipboard to get everything.
Repository: bk20dev/forest
Branch: main
Commit: 42b6b3ded72f
Files: 144
Total size: 247.3 KB
Directory structure:
gitextract__9jakuws/
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── .name
│ ├── AndroidProjectSystem.xml
│ ├── codeStyles/
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── compiler.xml
│ ├── deploymentTargetSelector.xml
│ ├── deviceManager.xml
│ ├── gradle.xml
│ ├── kotlinc.xml
│ ├── migrations.xml
│ ├── misc.xml
│ ├── runConfigurations.xml
│ └── vcs.xml
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── pl/
│ │ └── bartek537/
│ │ └── forest/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── pl/
│ │ │ └── bartek537/
│ │ │ └── forest/
│ │ │ ├── ForestApplication.kt
│ │ │ ├── core/
│ │ │ │ ├── data/
│ │ │ │ │ ├── repository/
│ │ │ │ │ │ └── DayRepositoryImpl.kt
│ │ │ │ │ └── source/
│ │ │ │ │ ├── DayDao.kt
│ │ │ │ │ ├── ForestDatabase.kt
│ │ │ │ │ └── util/
│ │ │ │ │ └── Converters.kt
│ │ │ │ ├── domain/
│ │ │ │ │ ├── model/
│ │ │ │ │ │ ├── Day.kt
│ │ │ │ │ │ ├── DaySettings.kt
│ │ │ │ │ │ └── StatsSummary.kt
│ │ │ │ │ ├── repository/
│ │ │ │ │ │ └── DayRepository.kt
│ │ │ │ │ └── usecase/
│ │ │ │ │ ├── DayUseCases.kt
│ │ │ │ │ ├── GetDay.kt
│ │ │ │ │ ├── GetDayImpl.kt
│ │ │ │ │ ├── IncrementStepCount.kt
│ │ │ │ │ └── IncrementStepCountImpl.kt
│ │ │ │ └── presentation/
│ │ │ │ ├── ActivityRecognitionPermissionFragment.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── OnboardingActivity.kt
│ │ │ │ └── SplashActivity.kt
│ │ │ ├── progress/
│ │ │ │ ├── ProgressFragment.kt
│ │ │ │ ├── ProgressState.kt
│ │ │ │ └── ProgressViewModel.kt
│ │ │ ├── service/
│ │ │ │ ├── StepCounterController.kt
│ │ │ │ ├── StepCounterEvent.kt
│ │ │ │ ├── StepCounterService.kt
│ │ │ │ ├── StepCounterServiceLauncher.kt
│ │ │ │ └── StepCounterState.kt
│ │ │ ├── settings/
│ │ │ │ ├── SettingsActivity.kt
│ │ │ │ ├── SettingsFragment.kt
│ │ │ │ ├── SettingsViewModel.kt
│ │ │ │ ├── data/
│ │ │ │ │ ├── repository/
│ │ │ │ │ │ └── SettingsRepositoryImpl.kt
│ │ │ │ │ └── source/
│ │ │ │ │ ├── SettingsStore.kt
│ │ │ │ │ └── SettingsStoreImpl.kt
│ │ │ │ └── domain/
│ │ │ │ ├── model/
│ │ │ │ │ └── Settings.kt
│ │ │ │ ├── repository/
│ │ │ │ │ └── SettingsRepository.kt
│ │ │ │ └── usecase/
│ │ │ │ ├── GetSettings.kt
│ │ │ │ ├── SettingsUseCases.kt
│ │ │ │ └── UpdateDaySettings.kt
│ │ │ ├── stats/
│ │ │ │ ├── StatsFragment.kt
│ │ │ │ ├── domain/
│ │ │ │ │ └── usecase/
│ │ │ │ │ ├── GetFirstDate.kt
│ │ │ │ │ ├── GetSummary.kt
│ │ │ │ │ ├── GetWeek.kt
│ │ │ │ │ ├── StatsChartPageUseCases.kt
│ │ │ │ │ ├── StatsDetailsUseCases.kt
│ │ │ │ │ └── StatsSummaryUseCases.kt
│ │ │ │ ├── presentation/
│ │ │ │ │ ├── ChartAdapter.kt
│ │ │ │ │ ├── StatsChartFragment.kt
│ │ │ │ │ ├── StatsChartPageFragment.kt
│ │ │ │ │ ├── StatsChartPageViewModel.kt
│ │ │ │ │ ├── StatsChartState.kt
│ │ │ │ │ ├── StatsDetailsFragment.kt
│ │ │ │ │ ├── StatsDetailsState.kt
│ │ │ │ │ ├── StatsDetailsViewModel.kt
│ │ │ │ │ ├── StatsSummaryFragment.kt
│ │ │ │ │ ├── StatsSummaryState.kt
│ │ │ │ │ └── StatsSummaryViewModel.kt
│ │ │ │ └── util/
│ │ │ │ ├── ContextExtension.kt
│ │ │ │ ├── DayExtension.kt
│ │ │ │ └── LocalDateExtension.kt
│ │ │ └── trees/
│ │ │ ├── ForestFragment.kt
│ │ │ ├── ForestState.kt
│ │ │ ├── ForestViewModel.kt
│ │ │ └── domain/
│ │ │ └── usecase/
│ │ │ ├── ForestUseCases.kt
│ │ │ └── GetTreeCount.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── bubble_chart_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── chevron_left_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── chevron_right_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── conversion_path_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── directions_walk_fill0_wght400_grad0_opsz48.xml
│ │ │ ├── do_not_disturb_on_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── forest_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ ├── local_fire_department_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── nature_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── shape_chart_bar.xml
│ │ │ ├── shape_circle.xml
│ │ │ ├── shape_divider.xml
│ │ │ ├── shape_ground.xml
│ │ │ ├── show_chart_fill0_wght400_grad0_opsz24.xml
│ │ │ ├── stage_1.xml
│ │ │ ├── stage_2.xml
│ │ │ ├── stage_3.xml
│ │ │ ├── stage_4.xml
│ │ │ ├── stage_5.xml
│ │ │ ├── stage_6.xml
│ │ │ ├── steps_fill0_wght400_grad0_opsz24.xml
│ │ │ └── tree_collected.xml
│ │ ├── layout/
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_onboarding.xml
│ │ │ ├── activity_settings.xml
│ │ │ ├── fragment_activity_recognition_permission.xml
│ │ │ ├── fragment_forest.xml
│ │ │ ├── fragment_progress.xml
│ │ │ ├── fragment_stats.xml
│ │ │ ├── fragment_stats_chart.xml
│ │ │ ├── fragment_stats_details.xml
│ │ │ ├── fragment_stats_page_chart.xml
│ │ │ ├── fragment_stats_summary.xml
│ │ │ └── item_chart_bar.xml
│ │ ├── menu/
│ │ │ ├── bottom_navigation_menu.xml
│ │ │ └── main_menu.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── navigation/
│ │ │ ├── nav_graph.xml
│ │ │ └── onboarding_nav_graph.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ ├── values-night/
│ │ │ └── themes.xml
│ │ ├── values-v29/
│ │ │ └── themes.xml
│ │ └── xml/
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── settings.xml
│ └── test/
│ └── java/
│ └── pl/
│ └── bartek537/
│ └── forest/
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
================================================
FILE: .idea/.gitignore
================================================
# Default ignored files
/shelf/
/workspace.xml
================================================
FILE: .idea/.name
================================================
Forest
================================================
FILE: .idea/AndroidProjectSystem.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
================================================
FILE: .idea/codeStyles/Project.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
================================================
FILE: .idea/compiler.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>
================================================
FILE: .idea/deploymentTargetSelector.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
================================================
FILE: .idea/deviceManager.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
================================================
FILE: .idea/gradle.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
================================================
FILE: .idea/kotlinc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.0" />
</component>
</project>
================================================
FILE: .idea/migrations.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
================================================
FILE: .idea/misc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
================================================
FILE: .idea/runConfigurations.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
================================================
FILE: README.md
================================================
# Forest
Track your daily step count, stay healthy and fight the climate change, one step at a time.

## 🦁 Table of Contents
- [Forest](#forest)
- [🦁 Table of Contents](#-table-of-contents)
- [🌳 Inspiration](#-inspiration)
- [🥕 Features](#-features)
- [🐻❄️ Installation and First Launch](#-installation-and-first-launch)
- [🪴 Technologies](#-technologies)
- [🐌 Resources](#-resources)
## 🌳 Inspiration
A couple of years ago together with my friends, I took part in a programming competition. The
objective was to build a mobile app that solves a global problem. We didn't win, but the app we
built quickly spread in our families.
## 🥕 Features
<img src="https://user-images.githubusercontent.com/60577942/221682705-39a0e476-bb52-4257-8d3b-5e5a64e72424.gif" alt="application demo" width="270">
<br />
- Track your step count, burned calories, distance traveled and CO₂ saved
- Get rewarded by completing your daily goal and stay motivated
- Get handy notifications when your daily stats get updated
- View a daily history of your progress
- View a detailed summary of your overall progress
## 🐻❄️ Installation and First Launch
1. Download the **latest stable** application binary (.apk file) from
[Releases](https://github.com/bartek537/forest/releases).
2. Tap on the downloaded file and temporarily **allow installation from unknown sources**,
if prompted (turn it back off after installation).
3. On some devices you may encounter a Play Protect warning, but don't worry — the app is safe
to use and open-sourced. I'm just an unverified developer.
4. Click “Install” and wait for the app to install.
5. You are now good to go 🚀.
[//]: # (@formatter:off)
> [!CAUTION]
> On most devices you'll need to **turn off the app battery optimizations** for the
> app to count steps accurately. Forest uses a minimal amount of power and it won't impact your
> battery life.
>
> - Xiaomi devices running MIUI 14
> 1. Go to Settings > Apps > Manage apps > Forest > Battery saver.
> 2. Select "No restrictions".
>
> - Devices running Lineage OS 22.2
> 1. Go to Settings > Apps > All apps > Forest > App battery usage > Allow background usage
> (tap on the setting name to enter another menu).
> 2. Enable “Allow background usage”.
> 3. Change battery optimizations to “Unrestricted”.
[//]: # (@formatter:on)
## 🪴 Technologies
- Kotlin
- Flows and Coroutines
- Room
- Shared Preferences
- Navigation Component
- AndroidX Preference Library
- MVVM Design Pattern
- Clean Architecture
- Material You Dynamic Theming
## 🐌 Resources
https://www.notion.so/bartek537/Forest-223bc0c0f5be80bcbb4cc738eefe1ddd
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias libs.plugins.android.application
alias libs.plugins.kotlin.android
alias libs.plugins.devtools.ksp
}
android {
namespace = 'pl.bartek537.forest'
compileSdk = 36
defaultConfig {
applicationId "pl.bk20.forest"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
viewBinding = true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
coreLibraryDesugaringEnabled = true
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
}
}
dependencies {
implementation libs.koin.androidx
coreLibraryDesugaring libs.tools.desugar.jdk.libs
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
// User interface
implementation libs.android.material.design
implementation libs.androidx.activity.ktx
implementation libs.androidx.constraintlayout
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.lifecycle.service
implementation libs.androidx.lifecycle.viewmodel.ktx
implementation libs.androidx.navigation.fragment.ktx
implementation libs.androidx.navigation.ui.ktx
implementation libs.androidx.swiperefreshlayout
// Persistence
implementation libs.androidx.preference
implementation libs.androidx.room.ktx
ksp libs.androidx.room.compiler
// Test
testImplementation libs.junit
androidTestImplementation libs.androidx.test.espresso
androidTestImplementation libs.androidx.test.junit
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/androidTest/java/pl/bartek537/forest/ExampleInstrumentedTest.kt
================================================
package pl.bartek537.forest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("pl.bk20.forest", appContext.packageName)
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature
android:name="android.hardware.sensor.stepcounter"
android:required="true" />
<application
android:name=".ForestApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Forest"
tools:targetApi="31">
<service android:name=".service.StepCounterService" />
<receiver
android:name=".service.StepCounterServiceLauncher"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".core.presentation.SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".core.presentation.OnboardingActivity"
android:exported="false" />
<activity
android:name=".core.presentation.MainActivity"
android:exported="true"
android:launchMode="singleInstance" />
<activity
android:name=".settings.SettingsActivity"
android:exported="false"
android:launchMode="singleTop"
android:parentActivityName=".core.presentation.MainActivity" />
</application>
</manifest>
================================================
FILE: app/src/main/java/pl/bartek537/forest/ForestApplication.kt
================================================
package pl.bartek537.forest
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.preference.PreferenceManager
import androidx.room.Room
import com.google.android.material.color.DynamicColors
import kotlinx.coroutines.flow.MutableStateFlow
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.dsl.module
import pl.bartek537.forest.core.data.source.ForestDatabase
import pl.bartek537.forest.settings.data.source.SettingsStore
import pl.bartek537.forest.settings.data.source.SettingsStoreImpl
import java.time.LocalDate
val forestApplicationModule = module {
}
class ForestApplication : Application() {
lateinit var settingsStore: SettingsStore
lateinit var forestDatabase: ForestDatabase
val currentDate = MutableStateFlow<LocalDate>(LocalDate.now())
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@ForestApplication)
modules(forestApplicationModule)
}
DynamicColors.applyToActivitiesIfAvailable(this)
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
registerMidnightTimer()
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
settingsStore = SettingsStoreImpl(sharedPreferences)
forestDatabase = Room.databaseBuilder(
applicationContext,
ForestDatabase::class.java,
ForestDatabase.DATABASE_NAME
).build()
}
private fun registerMidnightTimer() {
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_TIME_TICK)
addAction(Intent.ACTION_TIME_CHANGED)
addAction(Intent.ACTION_TIMEZONE_CHANGED)
}
registerReceiver(midnightBroadcastReceiver, intentFilter)
}
private val midnightBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val today = LocalDate.now()
if (today != currentDate.value) {
currentDate.value = today
}
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/data/repository/DayRepositoryImpl.kt
================================================
package pl.bartek537.forest.core.data.repository
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.core.data.source.DayDao
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.core.domain.model.DaySettings
import pl.bartek537.forest.core.domain.repository.DayRepository
import java.time.LocalDate
class DayRepositoryImpl(
private val dao: DayDao
) : DayRepository {
override fun getTreeCount(): Flow<Int> {
return dao.getTreeCount()
}
override fun getFirstDay(): Flow<Day?> {
return dao.getFirstDay()
}
override fun getDay(date: LocalDate): Flow<Day?> {
return dao.getDay(date)
}
override suspend fun getAllDays(): List<Day> {
return dao.getAllDays()
}
override fun getDays(range: ClosedRange<LocalDate>): Flow<List<Day>> {
return dao.getDays(range.start, range.endInclusive)
}
override suspend fun upsertDay(day: Day) {
dao.upsertDay(day)
}
override suspend fun updateDaySettings(daySettings: DaySettings) {
dao.updateDaySettings(daySettings)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/data/source/DayDao.kt
================================================
package pl.bartek537.forest.core.data.source
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.core.domain.model.DaySettings
import java.time.LocalDate
@Dao
interface DayDao {
@Query("SELECT COUNT(*) FROM day WHERE steps >= goal")
fun getTreeCount(): Flow<Int>
@Query("SELECT * FROM day ORDER BY date ASC LIMIT 1")
fun getFirstDay(): Flow<Day?>
@Query("SELECT * FROM day WHERE date = :date")
fun getDay(date: LocalDate): Flow<Day?>
@Query("SELECT * FROM day")
suspend fun getAllDays(): List<Day>
@Query("SELECT * FROM day WHERE date BETWEEN :start AND :endInclusive")
fun getDays(start: LocalDate, endInclusive: LocalDate): Flow<List<Day>>
@Upsert
suspend fun upsertDay(day: Day)
@Update(entity = Day::class)
suspend fun updateDaySettings(day: DaySettings)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/data/source/ForestDatabase.kt
================================================
package pl.bartek537.forest.core.data.source
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import pl.bartek537.forest.core.data.source.util.Converters
import pl.bartek537.forest.core.domain.model.Day
@Database(entities = [Day::class], version = 1)
@TypeConverters(Converters::class)
abstract class ForestDatabase : RoomDatabase() {
abstract val dayDao: DayDao
companion object {
const val DATABASE_NAME = "forest_database"
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/data/source/util/Converters.kt
================================================
package pl.bartek537.forest.core.data.source.util
import androidx.room.TypeConverter
import java.time.LocalDate
@Suppress("unused")
class Converters {
@TypeConverter
fun localDateToTimestamp(date: LocalDate): Long {
return date.toEpochDay()
}
@TypeConverter
fun timestampToLocalDate(timestamp: Long): LocalDate {
return LocalDate.ofEpochDay(timestamp)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/model/Day.kt
================================================
package pl.bartek537.forest.core.domain.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import pl.bartek537.forest.settings.domain.model.Settings
import java.time.LocalDate
@Entity(tableName = "day")
data class Day(
@PrimaryKey val date: LocalDate,
val steps: Int = 0,
val goal: Int,
val height: Int = 188,
val weight: Int = 70,
val stepLength: Int = 72,
val pace: Double = 1.0
) {
companion object
val distanceTravelled
get() = run {
val distanceCentimeters = steps * stepLength
distanceCentimeters.toDouble() / 100_000
}
val calorieBurned
get() = run {
val modifier = height / 182.0 + weight / 70.0 - 1
0.04 * steps * pace * modifier
}
val carbonDioxideSaved
get() = run {
steps * 0.1925 / 1000.0
}
}
fun Day.Companion.of(date: LocalDate, settings: Settings, steps: Int = 0): Day {
return settings.run {
Day(
date = date,
steps = steps,
goal = dailyGoal,
height = height,
weight = weight,
stepLength = stepLength,
pace = pace
)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/model/DaySettings.kt
================================================
package pl.bartek537.forest.core.domain.model
import java.time.LocalDate
data class DaySettings(
val date: LocalDate,
val goal: Int,
val height: Int,
val weight: Int,
val stepLength: Int,
val pace: Double
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/model/StatsSummary.kt
================================================
package pl.bartek537.forest.core.domain.model
data class StatsSummary(
val treesCollected: Int = 0,
val stepsTaken: Long = 0L,
val calorieBurned: Double = 0.0,
val distanceTravelled: Double = 0.0,
val carbonDioxideSaved: Double = 0.0,
) {
companion object
}
fun StatsSummary.Companion.of(days: List<Day>): StatsSummary {
val treesCollected = days.count { it.steps >= it.goal }
val stepsTaken = days.sumOf { it.steps.toLong() }
val calorieBurned = days.sumOf { it.calorieBurned }
val distanceTravelled = days.sumOf { it.distanceTravelled }
val carbonDioxideSaved = days.sumOf { it.carbonDioxideSaved }
return StatsSummary(
treesCollected = treesCollected,
stepsTaken = stepsTaken,
calorieBurned = calorieBurned,
distanceTravelled = distanceTravelled,
carbonDioxideSaved = carbonDioxideSaved
)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/repository/DayRepository.kt
================================================
package pl.bartek537.forest.core.domain.repository
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.core.domain.model.DaySettings
import java.time.LocalDate
interface DayRepository {
fun getTreeCount(): Flow<Int>
fun getFirstDay(): Flow<Day?>
fun getDay(date: LocalDate): Flow<Day?>
suspend fun getAllDays(): List<Day>
fun getDays(range: ClosedRange<LocalDate>): Flow<List<Day>>
suspend fun upsertDay(day: Day)
suspend fun updateDaySettings(daySettings: DaySettings)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/usecase/DayUseCases.kt
================================================
package pl.bartek537.forest.core.domain.usecase
import pl.bartek537.forest.core.domain.repository.DayRepository
import pl.bartek537.forest.settings.domain.repository.SettingsRepository
class DayUseCases(
dayRepository: DayRepository,
settingsRepository: SettingsRepository
) {
val getDay: GetDay = GetDayImpl(dayRepository, settingsRepository)
val incrementStepCount: IncrementStepCount = IncrementStepCountImpl(dayRepository, getDay)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/usecase/GetDay.kt
================================================
package pl.bartek537.forest.core.domain.usecase
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.core.domain.model.Day
import java.time.LocalDate
interface GetDay {
operator fun invoke(date: LocalDate): Flow<Day>
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/usecase/GetDayImpl.kt
================================================
package pl.bartek537.forest.core.domain.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.core.domain.model.of
import pl.bartek537.forest.core.domain.repository.DayRepository
import pl.bartek537.forest.settings.domain.repository.SettingsRepository
import java.time.LocalDate
class GetDayImpl(
private val dayRepository: DayRepository,
private val settingsRepository: SettingsRepository,
) : GetDay {
override fun invoke(date: LocalDate): Flow<Day> {
val settingsFlow = settingsRepository.getSettings()
val dayFlow = dayRepository.getDay(date)
return settingsFlow.combine(dayFlow) { settings, day ->
day ?: Day.of(date, settings, steps = 0)
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/usecase/IncrementStepCount.kt
================================================
package pl.bartek537.forest.core.domain.usecase
import java.time.LocalDate
interface IncrementStepCount {
suspend operator fun invoke(date: LocalDate, by: Int)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/domain/usecase/IncrementStepCountImpl.kt
================================================
package pl.bartek537.forest.core.domain.usecase
import kotlinx.coroutines.flow.first
import pl.bartek537.forest.core.domain.repository.DayRepository
import java.time.LocalDate
class IncrementStepCountImpl(
private val repository: DayRepository,
private val getDayUseCase: GetDay
) : IncrementStepCount {
override suspend fun invoke(date: LocalDate, by: Int) {
val day = getDayUseCase(date).first()
val updatedDay = day.copy(steps = day.steps + by)
repository.upsertDay(updatedDay)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/presentation/ActivityRecognitionPermissionFragment.kt
================================================
package pl.bartek537.forest.core.presentation
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.FragmentActivityRecognitionPermissionBinding
class ActivityRecognitionPermissionFragment : Fragment() {
private var _binding: FragmentActivityRecognitionPermissionBinding? = null
private val binding get() = _binding!!
@RequiresApi(Build.VERSION_CODES.Q)
private val requestPermissionLauncher = registerForActivityResult(RequestPermission()) {
when (ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.ACTIVITY_RECOGNITION
)) {
PackageManager.PERMISSION_GRANTED -> openMainActivity()
PackageManager.PERMISSION_DENIED -> openPermissionSettings()
}
}
private fun openMainActivity() {
val action = R.id.action_activityRecognitionPermissionFragment_to_mainActivity
findNavController().navigate(action)
requireActivity().finish()
}
private fun openPermissionSettings() {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", requireContext().packageName, null)
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentActivityRecognitionPermissionBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
openMainActivity()
return
}
binding.buttonContinue.setOnClickListener {
requestPermission()
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun requestPermission() {
requestPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/presentation/MainActivity.kt
================================================
package pl.bartek537.forest.core.presentation
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.ActivityMainBinding
import pl.bartek537.forest.service.StepCounterService
import pl.bartek537.forest.settings.SettingsActivity
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.progressFragment,
R.id.statsFragment,
R.id.forestFragment,
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
binding.bottomNavigation.setupWithNavController(navController)
startStepCounterService()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
askForNotificationPermission()
}
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun askForNotificationPermission() {
val notificationPermission = android.Manifest.permission.POST_NOTIFICATIONS
val notificationPermissionStatus = ContextCompat
.checkSelfPermission(this, notificationPermission)
if (notificationPermissionStatus == PackageManager.PERMISSION_DENIED) {
requestPermissionLauncher.launch(notificationPermission)
}
}
private fun startStepCounterService() {
val intent = Intent(this, StepCounterService::class.java)
ContextCompat.startForegroundService(this, intent)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.settings -> {
openSettings()
true
}
else -> false
}
private fun openSettings() {
val settingsIntent = Intent(this, SettingsActivity::class.java)
startActivity(settingsIntent)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/presentation/OnboardingActivity.kt
================================================
package pl.bartek537.forest.core.presentation
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.ActivityOnboardingBinding
class OnboardingActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityOnboardingBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/core/presentation/SplashActivity.kt
================================================
package pl.bartek537.forest.core.presentation
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@SuppressLint("CustomSplashScreen")
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (shouldOpenOnboarding()) {
openOnboardingActivity()
} else {
openMainActivity()
}
finish()
}
private fun shouldOpenOnboarding(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return false
}
val permission = Manifest.permission.ACTIVITY_RECOGNITION
return !hasPermission(this, permission)
}
private fun openOnboardingActivity() {
val intent = Intent(this, OnboardingActivity::class.java)
startActivity(intent)
}
private fun openMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
@Suppress("SameParameterValue")
private fun hasPermission(context: Context, permission: String): Boolean {
val status = ContextCompat.checkSelfPermission(context, permission)
return status == PackageManager.PERMISSION_GRANTED
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/progress/ProgressFragment.kt
================================================
package pl.bartek537.forest.progress
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.FragmentProgressBinding
import java.text.DecimalFormat
class ProgressFragment : Fragment() {
private val viewModel: ProgressViewModel by activityViewModels { ProgressViewModel }
private var _binding: FragmentProgressBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentProgressBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.progress.collect { progress -> updateUserInterface(progress) }
}
}
}
private fun updateUserInterface(state: ProgressState) {
updateProgress(state)
updateTree(state)
updateTiles(state)
}
private fun updateProgress(state: ProgressState) = state.apply {
val numberFormat = DecimalFormat.getIntegerInstance()
val formattedStepCount = numberFormat.format(stepsTaken)
val dailyGoalStepCount = numberFormat.format(dailyGoal)
val dailyGoalText = getString(R.string.step_goal, dailyGoalStepCount)
binding.apply {
textStepCount.text = formattedStepCount
textDailyGoal.text = dailyGoalText
progressDailyGoal.max = dailyGoal
progressDailyGoal.progress = stepsTaken
}
}
private fun updateTree(state: ProgressState) = state.apply {
val treeResource = getTreeResource(stepsTaken.toDouble() / dailyGoal)
binding.imageTree.setImageResource(treeResource)
}
private fun updateTiles(state: ProgressState) = state.apply {
val calorieText = getString(
R.string.calorie_burned_format, calorieBurned
)
val distanceText = getString(
R.string.distance_travelled_format, distanceTravelled
)
val carbonDioxideText = getString(
R.string.carbon_dioxide_saved_format, carbonDioxideSaved
)
binding.apply {
textCalorieBurned.text = calorieText
textDistanceTravelled.text = distanceText
textCarbonDioxideSaved.text = carbonDioxideText
}
}
private fun getTreeResource(progress: Double) =
when {
progress < .2 -> R.drawable.stage_1
progress < .4 -> R.drawable.stage_2
progress < .6 -> R.drawable.stage_3
progress < .8 -> R.drawable.stage_4
progress < 1 -> R.drawable.stage_5
else -> R.drawable.stage_6
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/progress/ProgressState.kt
================================================
package pl.bartek537.forest.progress
import java.time.LocalDate
data class ProgressState(
val date: LocalDate,
val stepsTaken: Int,
val dailyGoal: Int,
val calorieBurned: Int,
val distanceTravelled: Double,
val carbonDioxideSaved: Double,
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/progress/ProgressViewModel.kt
================================================
package pl.bartek537.forest.progress
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import pl.bartek537.forest.ForestApplication
import pl.bartek537.forest.core.data.repository.DayRepositoryImpl
import pl.bartek537.forest.core.domain.usecase.DayUseCases
import pl.bartek537.forest.settings.data.repository.SettingsRepositoryImpl
import java.time.LocalDate
import kotlin.math.roundToInt
class ProgressViewModel(
private val dayUseCases: DayUseCases,
private val currentDateFlow: StateFlow<LocalDate>
) : ViewModel() {
private val _progress = MutableStateFlow(
ProgressState(
date = LocalDate.MIN,
stepsTaken = 0,
dailyGoal = 0,
calorieBurned = 0,
distanceTravelled = 0.0,
carbonDioxideSaved = 0.0,
)
)
val progress: StateFlow<ProgressState> = _progress.asStateFlow()
private var getProgressJob: Job? = null
init {
viewModelScope.launch {
currentDateFlow.collect { date ->
getProgress(date)
}
}
}
private fun getProgress(date: LocalDate) {
getProgressJob?.cancel()
getProgressJob = dayUseCases.getDay(date).onEach { day ->
_progress.value = progress.value.copy(
date = day.date,
stepsTaken = day.steps,
dailyGoal = day.goal,
calorieBurned = day.calorieBurned.roundToInt(),
distanceTravelled = day.distanceTravelled,
carbonDioxideSaved = day.carbonDioxideSaved,
)
}.launchIn(viewModelScope)
}
companion object Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val application = checkNotNull(extras[APPLICATION_KEY]) as ForestApplication
val settingsStore = application.settingsStore
val settingsRepository = SettingsRepositoryImpl(settingsStore)
val dayDatabase = application.forestDatabase
val dayRepository = DayRepositoryImpl(dayDatabase.dayDao)
val dayUseCases = DayUseCases(dayRepository, settingsRepository)
val currentDateFlow = application.currentDate
return ProgressViewModel(dayUseCases, currentDateFlow) as T
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/service/StepCounterController.kt
================================================
package pl.bartek537.forest.service
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import pl.bartek537.forest.core.domain.usecase.DayUseCases
import java.time.LocalDate
import kotlin.math.roundToInt
class StepCounterController(
private val dayUseCases: DayUseCases,
private val coroutineScope: CoroutineScope,
currentDateFlow: StateFlow<LocalDate>,
) {
private val _stats = MutableStateFlow(StepCounterState(LocalDate.now(), 0, 0, 0.0, 0))
val stats: StateFlow<StepCounterState> = _stats.asStateFlow()
private var getStatsJob: Job? = null
init {
coroutineScope.launch {
currentDateFlow.collect { getStats(it) }
}
}
private fun getStats(date: LocalDate) {
getStatsJob?.cancel()
getStatsJob = dayUseCases.getDay(date).onEach { day ->
_stats.value = day.run {
StepCounterState(
date = date,
steps = steps,
goal = goal,
distanceTravelled = distanceTravelled,
calorieBurned = calorieBurned.roundToInt()
)
}
}.launchIn(coroutineScope)
}
private val rawStepSensorReadings = MutableStateFlow(StepCounterEvent(0, LocalDate.MIN))
private var previousStepCount: Int? = null
init {
rawStepSensorReadings.drop(1).onEach { event ->
val stepCountDifference = event.stepCount - (previousStepCount ?: event.stepCount)
previousStepCount = event.stepCount
dayUseCases.incrementStepCount(event.eventDate, stepCountDifference)
}.launchIn(coroutineScope)
}
fun onStepCountChanged(newStepCount: Int, eventDate: LocalDate) {
rawStepSensorReadings.value = StepCounterEvent(newStepCount, eventDate)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/service/StepCounterEvent.kt
================================================
package pl.bartek537.forest.service
import java.time.LocalDate
data class StepCounterEvent(
val stepCount: Int,
val eventDate: LocalDate,
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/service/StepCounterService.kt
================================================
package pl.bartek537.forest.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Build.VERSION_CODES
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import pl.bartek537.forest.ForestApplication
import pl.bartek537.forest.R
import pl.bartek537.forest.core.data.repository.DayRepositoryImpl
import pl.bartek537.forest.core.domain.usecase.DayUseCases
import pl.bartek537.forest.core.presentation.MainActivity
import pl.bartek537.forest.settings.data.repository.SettingsRepositoryImpl
import java.time.LocalDate
class StepCounterService : LifecycleService(), SensorEventListener {
private lateinit var sensorManager: SensorManager
private lateinit var controller: StepCounterController
companion object {
private const val NOTIFICATION_CHANNEL_ID = "step_counter_channel"
private const val NOTIFICATION_ID = 0x1
private const val PENDING_INTENT_ID = 0x1
}
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
val notificationChannel = createNotificationChannel()
registerNotificationChannel(notificationChannel)
}
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
registerStepCounter(sensorManager)
// Initialise controller
val application = application as ForestApplication
val settingsStore = application.settingsStore
val settingsRepository = SettingsRepositoryImpl(settingsStore)
val dayDatabase = application.forestDatabase
val dayRepository = DayRepositoryImpl(dayDatabase.dayDao)
val dayUseCases = DayUseCases(dayRepository, settingsRepository)
controller = StepCounterController(dayUseCases, lifecycleScope, application.currentDate)
// Create notification
val notification = createNotification(controller.stats.value)
startForeground(NOTIFICATION_ID, notification)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
controller.stats.collect {
val updatedNotification = createNotification(it)
notificationManager.notify(NOTIFICATION_ID, updatedNotification)
}
}
}
}
private fun createNotification(state: StepCounterState): Notification = state.run {
val title = resources.getQuantityString(R.plurals.step_count, steps, steps)
val progress = if (goal == 0) 0 else steps * 100 / goal
val content = getString(
R.string.step_counter_stats, calorieBurned, distanceTravelled, progress
)
NotificationCompat.Builder(this@StepCounterService, NOTIFICATION_CHANNEL_ID)
.setContentIntent(launchApplicationPendingIntent)
.setSmallIcon(R.drawable.nature_fill0_wght400_grad0_opsz24)
.setContentTitle(title)
.setContentText(content)
.setOnlyAlertOnce(true)
.setOngoing(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSilent(true)
.build()
}
private val launchApplicationPendingIntent
get(): PendingIntent {
val intent = Intent(applicationContext, MainActivity::class.java)
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
return PendingIntent.getActivity(this, PENDING_INTENT_ID, intent, flags)
}
private fun registerStepCounter(sensorManager: SensorManager) {
val stepCounterSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
stepCounterSensor?.let {
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL)
}
}
override fun onSensorChanged(event: SensorEvent?) {
event?.let {
val eventStepCount = it.values[0].toInt()
controller.onStepCountChanged(eventStepCount, LocalDate.now())
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
override fun onDestroy() {
super.onDestroy()
sensorManager.unregisterListener(this)
}
@RequiresApi(VERSION_CODES.O)
private fun createNotificationChannel(): NotificationChannel {
val name = getString(R.string.step_counter_channel)
val importance = NotificationManager.IMPORTANCE_DEFAULT
return NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply {
setShowBadge(false)
}
}
@RequiresApi(VERSION_CODES.O)
private fun registerNotificationChannel(channel: NotificationChannel) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/service/StepCounterServiceLauncher.kt
================================================
package pl.bartek537.forest.service
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
class StepCounterServiceLauncher : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
context?.run {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED && hasPermissions(context)) {
val launchIntent = Intent(applicationContext, StepCounterService::class.java)
ContextCompat.startForegroundService(applicationContext, launchIntent)
}
}
}
private fun hasPermissions(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (!hasPermission(context, Manifest.permission.ACTIVITY_RECOGNITION)) {
return false
}
}
return true
}
@Suppress("SameParameterValue")
private fun hasPermission(context: Context, permission: String): Boolean {
val status = ContextCompat.checkSelfPermission(context, permission)
return status == PackageManager.PERMISSION_GRANTED
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/service/StepCounterState.kt
================================================
package pl.bartek537.forest.service
import java.time.LocalDate
data class StepCounterState(
val date: LocalDate,
val steps: Int,
val goal: Int,
val distanceTravelled: Double,
val calorieBurned: Int
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/SettingsActivity.kt
================================================
package pl.bartek537.forest.settings
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import pl.bartek537.forest.databinding.ActivitySettingsBinding
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setupActionBar(binding)
}
private fun setupActionBar(binding: ActivitySettingsBinding) {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/SettingsFragment.kt
================================================
package pl.bartek537.forest.settings
import android.os.Bundle
import android.text.InputType
import androidx.fragment.app.activityViewModels
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import pl.bartek537.forest.R
class SettingsFragment : PreferenceFragmentCompat() {
private val viewModel: SettingsViewModel by activityViewModels { SettingsViewModel }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.observeSettingsChanges()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
val dailyGoalPreference = preferenceManager.findPreference<EditTextPreference>("daily_goal")
dailyGoalPreference?.summaryProvider = Preference.SummaryProvider<EditTextPreference> {
val dailyGoal = it.text?.toIntOrNull() ?: 0
resources.getQuantityString(R.plurals.daily_goal_summary, dailyGoal, dailyGoal)
}
val numericPreferenceKeys = listOf("daily_goal", "step_length", "height", "weight")
numericPreferenceKeys.forEach {
val preference = preferenceManager.findPreference<EditTextPreference>(it)
preference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER
}
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/SettingsViewModel.kt
================================================
package pl.bartek537.forest.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import pl.bartek537.forest.ForestApplication
import pl.bartek537.forest.core.data.repository.DayRepositoryImpl
import pl.bartek537.forest.core.domain.model.DaySettings
import pl.bartek537.forest.settings.data.repository.SettingsRepositoryImpl
import pl.bartek537.forest.settings.domain.usecase.SettingsUseCases
import java.time.LocalDate
class SettingsViewModel(
private val settingsUseCases: SettingsUseCases
) : ViewModel() {
private var observeSettingsChangesJob: Job? = null
fun observeSettingsChanges() {
observeSettingsChangesJob?.cancel()
observeSettingsChangesJob = settingsUseCases.getSettings().onEach {
settingsUseCases.updateDaySettings(
DaySettings(
date = LocalDate.now(),
goal = it.dailyGoal,
height = it.height,
weight = it.weight,
stepLength = it.stepLength,
pace = it.pace
)
)
}.launchIn(viewModelScope)
}
companion object Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val application = checkNotNull(extras[APPLICATION_KEY]) as ForestApplication
val settingsStore = application.settingsStore
val settingsRepository = SettingsRepositoryImpl(settingsStore)
val dayDatabase = application.forestDatabase
val dayRepository = DayRepositoryImpl(dayDatabase.dayDao)
val settingsUseCases = SettingsUseCases(settingsRepository, dayRepository)
return SettingsViewModel(settingsUseCases) as T
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/data/repository/SettingsRepositoryImpl.kt
================================================
package pl.bartek537.forest.settings.data.repository
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.settings.data.source.SettingsStore
import pl.bartek537.forest.settings.domain.model.Settings
import pl.bartek537.forest.settings.domain.repository.SettingsRepository
class SettingsRepositoryImpl(
private val settingsStore: SettingsStore
) : SettingsRepository {
override fun getSettings(): Flow<Settings> {
return settingsStore.getSettings()
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/data/source/SettingsStore.kt
================================================
package pl.bartek537.forest.settings.data.source
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.settings.domain.model.Settings
interface SettingsStore {
fun getSettings(): Flow<Settings>
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/data/source/SettingsStoreImpl.kt
================================================
package pl.bartek537.forest.settings.data.source
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import pl.bartek537.forest.settings.domain.model.Settings
class SettingsStoreImpl(
private val sharedPreferences: SharedPreferences
) : SettingsStore, OnSharedPreferenceChangeListener {
private val settings: MutableStateFlow<Settings>
init {
val parsedSettings = parseSettings(sharedPreferences)
settings = MutableStateFlow(parsedSettings)
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun getSettings(): Flow<Settings> {
return settings.asStateFlow()
}
private fun parseSettings(sharedPreferences: SharedPreferences): Settings =
sharedPreferences.run {
Settings(
dailyGoal = getNumericString("daily_goal", 0),
stepLength = getNumericString("step_length", 0),
height = getNumericString("height", 0),
weight = getNumericString("weight", 0),
pace = getNumericString("pace", 0.0)
)
}
private fun SharedPreferences.getNumericString(key: String, defaultValue: Int): Int =
getString(key, "")?.toIntOrNull() ?: defaultValue
private fun SharedPreferences.getNumericString(key: String, defaultValue: Double): Double =
getString(key, "")?.toDoubleOrNull() ?: defaultValue
override fun onSharedPreferenceChanged(
updatedSharedPreferences: SharedPreferences?,
key: String?
) {
settings.value = parseSettings(sharedPreferences)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/domain/model/Settings.kt
================================================
package pl.bartek537.forest.settings.domain.model
data class Settings(
val dailyGoal: Int,
val stepLength: Int,
val height: Int,
val weight: Int,
val pace: Double
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/domain/repository/SettingsRepository.kt
================================================
package pl.bartek537.forest.settings.domain.repository
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.settings.domain.model.Settings
interface SettingsRepository {
fun getSettings(): Flow<Settings>
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/domain/usecase/GetSettings.kt
================================================
package pl.bartek537.forest.settings.domain.usecase
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.settings.domain.model.Settings
import pl.bartek537.forest.settings.domain.repository.SettingsRepository
interface GetSettings {
operator fun invoke(): Flow<Settings>
}
class GetSettingsImpl(
private val repository: SettingsRepository
) : GetSettings {
override fun invoke(): Flow<Settings> {
return repository.getSettings()
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/domain/usecase/SettingsUseCases.kt
================================================
package pl.bartek537.forest.settings.domain.usecase
import pl.bartek537.forest.core.domain.repository.DayRepository
import pl.bartek537.forest.settings.domain.repository.SettingsRepository
class SettingsUseCases(
settingsRepository: SettingsRepository,
dayRepository: DayRepository,
) {
val getSettings: GetSettings = GetSettingsImpl(settingsRepository)
val updateDaySettings: UpdateDaySettings = UpdateDaySettingsImpl(dayRepository)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/settings/domain/usecase/UpdateDaySettings.kt
================================================
package pl.bartek537.forest.settings.domain.usecase
import pl.bartek537.forest.core.domain.model.DaySettings
import pl.bartek537.forest.core.domain.repository.DayRepository
interface UpdateDaySettings {
suspend operator fun invoke(daySettings: DaySettings)
}
class UpdateDaySettingsImpl(
private val dayRepository: DayRepository
) : UpdateDaySettings {
override suspend fun invoke(daySettings: DaySettings) {
dayRepository.updateDaySettings(daySettings)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/StatsFragment.kt
================================================
package pl.bartek537.forest.stats
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.FragmentStatsBinding
import pl.bartek537.forest.stats.presentation.StatsDetailsFragment
import pl.bartek537.forest.stats.presentation.StatsSummaryFragment
class StatsFragment : Fragment() {
private lateinit var binding: FragmentStatsBinding
companion object {
private val fragments = listOf(
R.string.details to { StatsDetailsFragment() },
R.string.summary to { StatsSummaryFragment() },
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStatsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val statsPageAdapter = StatsPageAdapter(this)
binding.pager.apply {
isUserInputEnabled = false
adapter = statsPageAdapter
}
TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->
val tabTitleRes = fragments[position].first
tab.text = getString(tabTitleRes)
}.attach()
}
class StatsPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment {
return fragments[position].second()
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/domain/usecase/GetFirstDate.kt
================================================
package pl.bartek537.forest.stats.domain.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import pl.bartek537.forest.core.domain.repository.DayRepository
import java.time.LocalDate
interface GetFirstDate {
operator fun invoke(): Flow<LocalDate>
}
class GetFirstDateImpl(
private val dayRepository: DayRepository
) : GetFirstDate {
override fun invoke(): Flow<LocalDate> {
return dayRepository.getFirstDay().map { it?.date ?: LocalDate.now() }
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/domain/usecase/GetSummary.kt
================================================
package pl.bartek537.forest.stats.domain.usecase
import pl.bartek537.forest.core.domain.model.StatsSummary
import pl.bartek537.forest.core.domain.model.of
import pl.bartek537.forest.core.domain.repository.DayRepository
interface GetSummary {
suspend operator fun invoke(): StatsSummary
}
class GetSummaryImpl(
private val dayRepository: DayRepository
) : GetSummary {
override suspend operator fun invoke(): StatsSummary {
val allDays = dayRepository.getAllDays()
return StatsSummary.of(allDays)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/domain/usecase/GetWeek.kt
================================================
package pl.bartek537.forest.stats.domain.usecase
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.core.domain.repository.DayRepository
import java.time.LocalDate
interface GetWeek {
operator fun invoke(startingAt: LocalDate): Flow<List<Day>>
}
class GetWeekImpl(
private val dayRepository: DayRepository
) : GetWeek {
override fun invoke(startingAt: LocalDate): Flow<List<Day>> {
val endingAt = startingAt.plusDays(6)
return dayRepository.getDays(startingAt..endingAt)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/domain/usecase/StatsChartPageUseCases.kt
================================================
package pl.bartek537.forest.stats.domain.usecase
import pl.bartek537.forest.core.domain.repository.DayRepository
class StatsChartPageUseCases(
dayRepository: DayRepository
) {
val getWeek: GetWeek = GetWeekImpl(dayRepository)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/domain/usecase/StatsDetailsUseCases.kt
================================================
package pl.bartek537.forest.stats.domain.usecase
import pl.bartek537.forest.core.domain.repository.DayRepository
class StatsDetailsUseCases(
dayRepository: DayRepository
) {
val getFirstDate: GetFirstDate = GetFirstDateImpl(dayRepository)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/domain/usecase/StatsSummaryUseCases.kt
================================================
package pl.bartek537.forest.stats.domain.usecase
import pl.bartek537.forest.core.domain.repository.DayRepository
class StatsSummaryUseCases(
dayRepository: DayRepository
) {
val getSummary: GetSummary = GetSummaryImpl(dayRepository)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/ChartAdapter.kt
================================================
package pl.bartek537.forest.stats.presentation
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import pl.bartek537.forest.databinding.ItemChartBarBinding
import pl.bartek537.forest.stats.util.getThemeColor
class ChartAdapter<T>(
private val listener: OnValueSelected<T>
) : ListAdapter<ChartAdapter.ChartValue<T>, ChartAdapter.ChartItemViewHolder<T>>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChartItemViewHolder<T> {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemChartBarBinding.inflate(layoutInflater, parent, false)
return ChartItemViewHolder(binding)
}
override fun onBindViewHolder(holder: ChartItemViewHolder<T>, position: Int) {
val value = getItem(position)
holder.bind(value, listener)
}
class ChartItemViewHolder<T>(
private val binding: ItemChartBarBinding
) : ViewHolder(binding.root) {
fun bind(chartValue: ChartValue<T>, listener: OnValueSelected<T>) {
binding.root.setOnClickListener { listener.onSelect(chartValue) }
binding.textSupporting.apply {
text = chartValue.label
val color = context.getThemeColor(chartValue.textColor)
setTextColor(color)
}
binding.barFilled.apply {
val color = context.getThemeColor(chartValue.barColor)
backgroundTintList = ColorStateList.valueOf(color)
val params = layoutParams as ConstraintLayout.LayoutParams
params.matchConstraintPercentHeight = chartValue.value.toFloat()
requestLayout()
}
}
}
private class DiffCallback<T> : DiffUtil.ItemCallback<ChartValue<T>>() {
override fun areItemsTheSame(oldItem: ChartValue<T>, newItem: ChartValue<T>): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ChartValue<T>, newItem: ChartValue<T>): Boolean {
return oldItem == newItem
}
}
data class ChartValue<T>(
val id: T,
val value: Double,
val label: String,
@field:AttrRes
val barColor: Int,
@field:AttrRes
val textColor: Int,
)
fun interface OnValueSelected<T> {
fun onSelect(value: ChartValue<T>)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsChartFragment.kt
================================================
package pl.bartek537.forest.stats.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import kotlinx.coroutines.launch
import pl.bartek537.forest.databinding.FragmentStatsChartBinding
import java.time.LocalDate
import java.time.Period
import java.time.format.DateTimeFormatter
class StatsChartFragment : Fragment() {
private val statsDetailsViewModel: StatsDetailsViewModel by activityViewModels { StatsDetailsViewModel.Factory }
private lateinit var binding: FragmentStatsChartBinding
private lateinit var chartPageAdapter: ChartPageAdapter
private val dateFormatter = DateTimeFormatter.ofPattern("EEE, MMM dd")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
binding = FragmentStatsChartBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
chartPageAdapter = ChartPageAdapter(this)
binding.viewPagerChart.adapter = chartPageAdapter
binding.buttonPreviousDay.setOnClickListener { changeSelectedDate(offset = -1) }
binding.buttonNextDay.setOnClickListener { changeSelectedDate(offset = 1) }
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
statsDetailsViewModel.day.collect {
updateUserInterface(it.date, it.chartDateRange)
}
}
}
}
private fun changeSelectedDate(offset: Long) {
val currentDate = statsDetailsViewModel.day.value.date
statsDetailsViewModel.selectDay(currentDate.plusDays(offset))
}
private fun updateUserInterface(selectedDate: LocalDate, dateRange: ClosedRange<LocalDate>) {
binding.apply {
textSelectedDate.text = selectedDate.format(dateFormatter)
buttonPreviousDay.isVisible = selectedDate.isAfter(dateRange.start)
buttonNextDay.isVisible = selectedDate.isBefore(dateRange.endInclusive)
chartPageAdapter.dateRange = dateRange
scrollChartTo(selectedDate)
}
}
private fun scrollChartTo(
selectedDate: LocalDate,
) {
val pageIndex = chartPageAdapter.getPageContaining(selectedDate)
binding.viewPagerChart.currentItem = pageIndex
}
class ChartPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
var dateRange = LocalDate.now()..LocalDate.now()
fun getPageContaining(selectedDate: LocalDate): Int {
val period = Period.between(selectedDate, dateRange.endInclusive)
return (period.days / 7).coerceIn(0, itemCount)
}
override fun getItemCount(): Int = dateRange.run {
val period = Period.between(start, endInclusive)
return period.days / 7 + 1
}
override fun createFragment(position: Int): Fragment {
val fragment = StatsChartPageFragment()
fragment.arguments = Bundle().apply {
putLong(StatsChartPageFragment.ARG_PAGE_NUMBER, position.toLong())
}
return fragment
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsChartPageFragment.kt
================================================
package pl.bartek537.forest.stats.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.databinding.FragmentStatsPageChartBinding
import pl.bartek537.forest.stats.util.toChartValues
import java.lang.Integer.max
import java.time.LocalDate
class StatsChartPageFragment : Fragment() {
companion object {
const val ARG_PAGE_NUMBER = "__page_number"
}
private lateinit var binding: FragmentStatsPageChartBinding
private val statsChartPageViewModel: StatsChartPageViewModel by viewModels { StatsChartPageViewModel.Factory }
private val statsDetailsViewModel: StatsDetailsViewModel by activityViewModels { StatsDetailsViewModel.Factory }
private var pageNumber: Long = 0
private val chartAdapter = ChartAdapter {
statsDetailsViewModel.selectDay(it.id)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pageNumber = arguments?.getLong(ARG_PAGE_NUMBER) ?: 0
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStatsPageChartBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.recyclerViewChart.apply {
adapter = chartAdapter
}
lifecycleScope.launch {
val activeDayFlow = statsDetailsViewModel.day
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
val weekFlow = statsChartPageViewModel.week
weekFlow.combine(activeDayFlow) { week, activeDay ->
updateUserInterface(week, activeDay.date)
}.collect()
}
launch {
activeDayFlow.collect {
updateSelectedWeek(it.chartDateRange.endInclusive)
}
}
}
}
}
private fun updateUserInterface(week: List<Day>, activeDate: LocalDate) {
val highestChartValue = week.maxOfOrNull { max(it.steps, it.goal) } ?: 1
val locale = resources.configuration.locales[0]
val chartValues = week.toChartValues(highestChartValue, locale, activeDate)
chartAdapter.submitList(chartValues)
}
private fun updateSelectedWeek(lastDate: LocalDate) {
val daysToSubtract = 7 * pageNumber + 6
val firstDate = lastDate.minusDays(daysToSubtract)
statsChartPageViewModel.selectWeek(firstDate)
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsChartPageViewModel.kt
================================================
package pl.bartek537.forest.stats.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import pl.bartek537.forest.ForestApplication
import pl.bartek537.forest.core.data.repository.DayRepositoryImpl
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.stats.domain.usecase.StatsChartPageUseCases
import pl.bartek537.forest.stats.util.alignWeek
import java.time.LocalDate
class StatsChartPageViewModel(
private val statsChartPageUseCases: StatsChartPageUseCases
) : ViewModel() {
private val _week = MutableStateFlow<List<Day>>(emptyList())
val week: StateFlow<List<Day>> = _week.asStateFlow()
private var getWeekJob: Job? = null
fun selectWeek(firstDate: LocalDate) {
getWeekJob?.cancel()
getWeekJob = viewModelScope.launch {
statsChartPageUseCases.getWeek(firstDate).collect { week ->
_week.value = week.alignWeek(firstDate)
}
}
}
companion object Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val application =
checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) as ForestApplication
val forestDatabase = application.forestDatabase
val dayRepository = DayRepositoryImpl(forestDatabase.dayDao)
val statsChartPageUseCases = StatsChartPageUseCases(dayRepository)
return StatsChartPageViewModel(statsChartPageUseCases) as T
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsChartState.kt
================================================
package pl.bartek537.forest.stats.presentation
import pl.bartek537.forest.core.domain.model.Day
import java.time.LocalDate
data class StatsChartState(
val week: List<Day>,
val dateRange: ClosedRange<LocalDate>
) {
companion object
}
fun StatsChartState.Companion.of(currentDate: LocalDate) = StatsChartState(
week = emptyList(),
dateRange = currentDate..currentDate
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsDetailsFragment.kt
================================================
package pl.bartek537.forest.stats.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.FragmentStatsDetailsBinding
class StatsDetailsFragment : Fragment() {
private val viewModel: StatsDetailsViewModel by activityViewModels { StatsDetailsViewModel }
private lateinit var binding: FragmentStatsDetailsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStatsDetailsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.day.collect { updateUserInterface(it) }
}
}
}
private fun updateUserInterface(state: StatsDetailsState) = state.apply {
val stepsText = resources.getQuantityString(
R.plurals.step_count_format, stepsTaken, stepsTaken
)
val calorieText = getString(
R.string.calorie_burned_format, calorieBurned
)
val distanceText = getString(
R.string.distance_travelled_format, distanceTravelled
)
val carbonDioxideText = getString(
R.string.carbon_dioxide_saved_format, carbonDioxideSaved
)
binding.apply {
textStepCount.text = stepsText
viewGroupTree.isVisible = treeCollected
textCalorieBurned.text = calorieText
textDistanceTravelled.text = distanceText
textCarbonDioxideSaved.text = carbonDioxideText
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsDetailsState.kt
================================================
package pl.bartek537.forest.stats.presentation
import java.time.LocalDate
data class StatsDetailsState(
val date: LocalDate,
val stepsTaken: Int,
val treeCollected: Boolean,
val calorieBurned: Int,
val distanceTravelled: Double,
val carbonDioxideSaved: Double,
val chartDateRange: ClosedRange<LocalDate>
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsDetailsViewModel.kt
================================================
package pl.bartek537.forest.stats.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import pl.bartek537.forest.ForestApplication
import pl.bartek537.forest.core.data.repository.DayRepositoryImpl
import pl.bartek537.forest.core.domain.usecase.DayUseCases
import pl.bartek537.forest.settings.data.repository.SettingsRepositoryImpl
import pl.bartek537.forest.stats.domain.usecase.StatsDetailsUseCases
import java.time.LocalDate
import kotlin.math.roundToInt
class StatsDetailsViewModel(
private val dayUseCases: DayUseCases,
statsDetailsUseCases: StatsDetailsUseCases,
currentDateFlow: StateFlow<LocalDate>
) : ViewModel() {
private val _day = MutableStateFlow(
StatsDetailsState(
date = LocalDate.MIN,
stepsTaken = 0,
treeCollected = false,
calorieBurned = 0,
distanceTravelled = 0.0,
carbonDioxideSaved = 0.0,
chartDateRange = currentDateFlow.value..currentDateFlow.value
)
)
val day: StateFlow<StatsDetailsState> = _day.asStateFlow()
init {
selectDay(currentDateFlow.value)
viewModelScope.launch {
val firstDateFlow = statsDetailsUseCases.getFirstDate()
firstDateFlow
.combine(currentDateFlow) { firstDate, currentDate ->
firstDate..currentDate
}.collect { dateRange ->
_day.value = day.value.copy(chartDateRange = dateRange)
}
}
}
private var selectDateJob: Job? = null
fun selectDay(date: LocalDate) {
selectDateJob?.cancel()
selectDateJob = dayUseCases.getDay(date).onEach {
_day.value = day.value.copy(
date = it.date,
stepsTaken = it.steps,
treeCollected = it.steps >= it.goal,
calorieBurned = it.calorieBurned.roundToInt(),
distanceTravelled = it.distanceTravelled,
carbonDioxideSaved = it.carbonDioxideSaved
)
}.launchIn(viewModelScope)
}
companion object Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val application = checkNotNull(extras[APPLICATION_KEY]) as ForestApplication
val dayDatabase = application.forestDatabase
val dayRepository = DayRepositoryImpl(dayDatabase.dayDao)
val settingsStore = application.settingsStore
val settingsRepository = SettingsRepositoryImpl(settingsStore)
val dayUseCases = DayUseCases(dayRepository, settingsRepository)
val statsDetailsUseCases = StatsDetailsUseCases(dayRepository)
return StatsDetailsViewModel(
dayUseCases,
statsDetailsUseCases,
application.currentDate
) as T
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsSummaryFragment.kt
================================================
package pl.bartek537.forest.stats.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.FragmentStatsSummaryBinding
import kotlin.math.roundToInt
class StatsSummaryFragment : Fragment() {
private lateinit var binding: FragmentStatsSummaryBinding
private val viewModel: StatsSummaryViewModel by viewModels { StatsSummaryViewModel }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStatsSummaryBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.swipeRefreshContainer.setOnRefreshListener {
viewModel.refreshStatsSummary()
}
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.statsSummary.collect { updateUserInterface(it) }
}
}
}
private fun updateUserInterface(state: StatsSummaryState) = state.apply {
val treesText = resources.getQuantityString(
R.plurals.trees_collected_format, treesCollected, treesCollected
)
val stepsText = resources.getQuantityString(
R.plurals.step_count_format, stepsTaken.toInt(), stepsTaken
)
val calorieText = getString(
R.string.calorie_burned_format, calorieBurned.roundToInt()
)
val distanceText = getString(
R.string.distance_travelled_format, distanceTravelled
)
val carbonDioxideText = getString(
R.string.carbon_dioxide_saved_format, carbonDioxideSaved
)
binding.apply {
swipeRefreshContainer.isRefreshing = state.isRefreshing
textTreesCollected.text = treesText
textStepCount.text = stepsText
textCalorieBurned.text = calorieText
textDistanceTravelled.text = distanceText
textCarbonDioxideSaved.text = carbonDioxideText
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsSummaryState.kt
================================================
package pl.bartek537.forest.stats.presentation
data class StatsSummaryState(
val isRefreshing: Boolean = false,
val treesCollected: Int = 0,
val stepsTaken: Long = 0L,
val calorieBurned: Double = 0.0,
val distanceTravelled: Double = 0.0,
val carbonDioxideSaved: Double = 0.0,
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/presentation/StatsSummaryViewModel.kt
================================================
package pl.bartek537.forest.stats.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import pl.bartek537.forest.ForestApplication
import pl.bartek537.forest.core.data.repository.DayRepositoryImpl
import pl.bartek537.forest.stats.domain.usecase.StatsSummaryUseCases
class StatsSummaryViewModel(
private val statsSummaryUseCases: StatsSummaryUseCases
) : ViewModel() {
private val _statsStatsSummary = MutableStateFlow(StatsSummaryState())
val statsSummary: StateFlow<StatsSummaryState> = _statsStatsSummary.asStateFlow()
init {
refreshStatsSummary()
}
private var refreshStatsSummaryJob: Job? = null
fun refreshStatsSummary() {
refreshStatsSummaryJob?.cancel()
refreshStatsSummaryJob = viewModelScope.launch {
_statsStatsSummary.value = statsSummary.value.copy(
isRefreshing = true
)
val updatedSummary = statsSummaryUseCases.getSummary()
updatedSummary.run {
_statsStatsSummary.value = statsSummary.value.copy(
isRefreshing = false,
treesCollected = treesCollected,
stepsTaken = stepsTaken,
calorieBurned = calorieBurned,
distanceTravelled = distanceTravelled,
carbonDioxideSaved = carbonDioxideSaved,
)
}
}
}
companion object Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val application =
checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) as ForestApplication
val dayDatabase = application.forestDatabase
val dayRepository = DayRepositoryImpl(dayDatabase.dayDao)
val statsSummaryUseCases = StatsSummaryUseCases(dayRepository)
return StatsSummaryViewModel(statsSummaryUseCases) as T
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/util/ContextExtension.kt
================================================
package pl.bartek537.forest.stats.util
import android.content.Context
import android.util.TypedValue
import androidx.annotation.AttrRes
fun Context.getThemeColor(
@AttrRes attrColor: Int
): Int {
val typedValue = TypedValue()
theme.resolveAttribute(attrColor, typedValue, true)
return typedValue.data
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/util/DayExtension.kt
================================================
package pl.bartek537.forest.stats.util
import pl.bartek537.forest.core.domain.model.Day
import java.time.LocalDate
fun List<Day>.alignWeek(
firstDay: LocalDate,
lastDay: LocalDate = firstDay.plusDays(6),
): List<Day> {
val alignedWeek = mutableListOf<Day>()
for (date in firstDay..lastDay) {
val currentDay = singleOrNull { it.date == date }
alignedWeek.add(currentDay ?: Day(date, goal = 0))
}
return alignedWeek
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/stats/util/LocalDateExtension.kt
================================================
package pl.bartek537.forest.stats.util
import com.google.android.material.R
import pl.bartek537.forest.core.domain.model.Day
import pl.bartek537.forest.stats.presentation.ChartAdapter
import java.time.LocalDate
import java.time.format.TextStyle
import java.util.*
operator fun ClosedRange<LocalDate>.iterator() = object : Iterator<LocalDate> {
private var current = start.minusDays(1)
override fun hasNext(): Boolean {
return current.isBefore(endInclusive)
}
override fun next(): LocalDate {
if (current.isBefore(endInclusive)) {
current = current.plusDays(1)
}
return current
}
}
fun List<Day>.toChartValues(
max: Int,
locale: Locale,
activeDay: LocalDate
): List<ChartAdapter.ChartValue<LocalDate>> = map {
val value = it.steps / max.toDouble()
val weekdayName = it.date.dayOfWeek.getDisplayName(TextStyle.SHORT, locale)
val isSelected = it.date.isEqual(activeDay)
val barColor =
if (isSelected) android.R.attr.colorPrimary
else R.attr.colorPrimaryContainer
val textColor =
if (isSelected) android.R.attr.colorPrimary
else R.attr.colorOnSurface
ChartAdapter.ChartValue(
it.date,
value = value,
label = weekdayName,
barColor = barColor,
textColor = textColor
)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/trees/ForestFragment.kt
================================================
package pl.bartek537.forest.trees
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import pl.bartek537.forest.R
import pl.bartek537.forest.databinding.FragmentForestBinding
import kotlin.random.Random
class ForestFragment : Fragment() {
private val viewModel: ForestViewModel by viewModels { ForestViewModel.Factory }
private lateinit var binding: FragmentForestBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentForestBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.trees.collect { updateUserInterface(it) }
}
}
}
private fun updateUserInterface(forestState: ForestState) {
val treeCount = forestState.treeCount
binding.apply {
textTreesCollected.text = treeCount.toString()
textTreesCollectedLabel.text = resources.getQuantityString(R.plurals.trees, treeCount)
}
generateTrees(forestState.treeCount)
}
private fun generateTrees(treeCount: Int) {
val parentLayout = binding.constraintLayoutTrees
parentLayout.removeAllViews()
val gapCount = treeCount + 1
repeat(treeCount) {
val fixedPosition = (it + 1.0) / gapCount
val randomOffset = (Random.nextDouble() - 0.5) / 5
val horizontalPosition = fixedPosition + randomOffset
createTree(parentLayout, horizontalPosition)
}
}
private fun createTree(parentLayout: ConstraintLayout, horizontalPosition: Double) {
val treeImageView = ImageView(context)
treeImageView.setImageResource(R.drawable.tree_collected)
parentLayout.addView(treeImageView)
treeImageView.updateLayoutParams<ConstraintLayout.LayoutParams> {
startToStart = parentLayout.id
endToEnd = parentLayout.id
bottomToBottom = parentLayout.id
horizontalBias = horizontalPosition.toFloat()
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/trees/ForestState.kt
================================================
package pl.bartek537.forest.trees
data class ForestState(
val treeCount: Int
)
================================================
FILE: app/src/main/java/pl/bartek537/forest/trees/ForestViewModel.kt
================================================
package pl.bartek537.forest.trees
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import pl.bartek537.forest.ForestApplication
import pl.bartek537.forest.core.data.repository.DayRepositoryImpl
import pl.bartek537.forest.trees.domain.usecase.ForestUseCases
class ForestViewModel(
forestUseCases: ForestUseCases
) : ViewModel() {
private val _trees = MutableStateFlow(ForestState(treeCount = 0))
val trees: StateFlow<ForestState> = _trees.asStateFlow()
init {
viewModelScope.launch {
forestUseCases.getTreeCount().collect {
_trees.value = _trees.value.copy(
treeCount = it
)
}
}
}
object Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val application = checkNotNull(extras[APPLICATION_KEY]) as ForestApplication
val forestDatabase = application.forestDatabase
val dayRepository = DayRepositoryImpl(forestDatabase.dayDao)
val forestUseCases = ForestUseCases(dayRepository)
return ForestViewModel(forestUseCases) as T
}
}
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/trees/domain/usecase/ForestUseCases.kt
================================================
package pl.bartek537.forest.trees.domain.usecase
import pl.bartek537.forest.core.domain.repository.DayRepository
class ForestUseCases(
dayRepository: DayRepository
) {
val getTreeCount: GetTreeCount = GetTreeCountImpl(dayRepository)
}
================================================
FILE: app/src/main/java/pl/bartek537/forest/trees/domain/usecase/GetTreeCount.kt
================================================
package pl.bartek537.forest.trees.domain.usecase
import kotlinx.coroutines.flow.Flow
import pl.bartek537.forest.core.domain.repository.DayRepository
interface GetTreeCount {
operator fun invoke(): Flow<Int>
}
class GetTreeCountImpl(
private val dayRepository: DayRepository
) : GetTreeCount {
override fun invoke(): Flow<Int> {
return dayRepository.getTreeCount()
}
}
================================================
FILE: app/src/main/res/drawable/bubble_chart_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M280,720q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM280,640q33,0 56.5,-23.5T360,560q0,-33 -23.5,-56.5T280,480q-33,0 -56.5,23.5T200,560q0,33 23.5,56.5T280,640ZM660,560q-92,0 -156,-64t-64,-156q0,-92 64,-156t156,-64q92,0 156,64t64,156q0,92 -64,156t-156,64ZM580,840q-50,0 -85,-35t-35,-85q0,-50 35,-85t85,-35q50,0 85,35t35,85q0,50 -35,85t-85,35ZM660,480q59,0 99.5,-40.5T800,340q0,-59 -40.5,-99.5T660,200q-59,0 -99.5,40.5T520,340q0,59 40.5,99.5T660,480ZM580,760q17,0 28.5,-11.5T620,720q0,-17 -11.5,-28.5T580,680q-17,0 -28.5,11.5T540,720q0,17 11.5,28.5T580,760ZM660,340ZM280,560ZM580,720Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/chevron_left_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="m14,18 l-6,-6 6,-6 1.4,1.4 -4.6,4.6 4.6,4.6Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/chevron_right_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9.4,18 L8,16.6l4.6,-4.6L8,7.4 9.4,6l6,6Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/conversion_path_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,21q-0.975,0 -1.75,-0.562 -0.775,-0.563 -1.075,-1.438H11q-1.65,0 -2.825,-1.175Q7,16.65 7,15q0,-1.65 1.175,-2.825Q9.35,11 11,11h2q0.825,0 1.413,-0.588Q15,9.825 15,9t-0.587,-1.413Q13.825,7 13,7H7.825q-0.325,0.875 -1.087,1.438Q5.975,9 5,9q-1.25,0 -2.125,-0.875T2,6q0,-1.25 0.875,-2.125T5,3q0.975,0 1.738,0.562Q7.5,4.125 7.825,5H13q1.65,0 2.825,1.175Q17,7.35 17,9q0,1.65 -1.175,2.825Q14.65,13 13,13h-2q-0.825,0 -1.412,0.587Q9,14.175 9,15q0,0.825 0.588,1.413Q10.175,17 11,17h5.175q0.325,-0.875 1.088,-1.438Q18.025,15 19,15q1.25,0 2.125,0.875T22,18q0,1.25 -0.875,2.125T19,21ZM5,7q0.425,0 0.713,-0.287Q6,6.425 6,6t-0.287,-0.713Q5.425,5 5,5t-0.713,0.287Q4,5.575 4,6t0.287,0.713Q4.575,7 5,7Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/directions_walk_fill0_wght400_grad0_opsz48.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FF000000"
android:pathData="m13.9,46 l5.8,-29.3 -5.05,2.15v6.65H11.6v-8.65l9.6,-4.05q0.7,-0.3 1.475,-0.375 0.775,-0.075 1.525,0.075 0.85,0.15 1.475,0.55 0.625,0.4 1.025,1l2.1,3.3q1.55,2.4 3.875,3.775T37.65,22.5v3q-3.5,-0.1 -6.175,-1.525Q28.8,22.55 26.65,19.6l-1.9,7.6 4.6,4.15V46h-3V34l-5.4,-4.9L17,46ZM27,10.3q-1.5,0 -2.575,-1.075Q23.35,8.15 23.35,6.65q0,-1.5 1.075,-2.575Q25.5,3 27,3q1.5,0 2.575,1.075Q30.65,5.15 30.65,6.65q0,1.5 -1.075,2.575Q28.5,10.3 27,10.3Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/do_not_disturb_on_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7,13h10v-2L7,11ZM12,22q-2.075,0 -3.9,-0.788 -1.825,-0.787 -3.175,-2.137 -1.35,-1.35 -2.137,-3.175Q2,14.075 2,12t0.788,-3.9q0.787,-1.825 2.137,-3.175 1.35,-1.35 3.175,-2.138Q9.925,2 12,2t3.9,0.787q1.825,0.788 3.175,2.138 1.35,1.35 2.137,3.175Q22,9.925 22,12t-0.788,3.9q-0.787,1.825 -2.137,3.175 -1.35,1.35 -3.175,2.137Q14.075,22 12,22ZM12,20q3.35,0 5.675,-2.325Q20,15.35 20,12q0,-3.35 -2.325,-5.675Q15.35,4 12,4 8.65,4 6.325,6.325 4,8.65 4,12q0,3.35 2.325,5.675Q8.65,20 12,20ZM12,12Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/forest_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7,22v-4L0,18l3.85,-6L2,12L9,2l3,4.3L15,2l7,10h-1.85L24,18h-7v4h-4v-4h-2v4ZM16.725,16h3.625l-3.875,-6h1.675L15,5.5l-1.775,2.525L16,12h-1.85ZM3.65,16h10.7l-3.875,-6h1.675L9,5.5 5.85,10h1.675ZM3.65,16h3.875L5.85,16h6.3,-1.675 3.875ZM16.725,16L14.15,16 16,16h-2.775,4.925 -1.675,3.875ZM13,18h4,-4ZM18.025,18Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="319"
android:viewportHeight="188">
<group android:scaleX="0.64"
android:scaleY="0.37717867"
android:translateX="57.42"
android:translateY="58.545204">
<group>
<clip-path
android:pathData="M0,140h319v48h-319z"/>
<path
android:pathData="M159.5,188C247.59,188 319,177.26 319,164C319,150.74 247.59,140 159.5,140C71.41,140 0,150.74 0,164C0,177.26 71.41,188 159.5,188Z"
android:fillColor="#60A625"/>
<path
android:pathData="M159.5,173C192.64,173 219.5,168.97 219.5,164C219.5,159.03 192.64,155 159.5,155C126.36,155 99.5,159.03 99.5,164C99.5,168.97 126.36,173 159.5,173Z"
android:fillColor="#51901C"/>
</group>
<group>
<clip-path
android:pathData="M77,0h164v164h-164z"/>
<path
android:pathData="M161.97,90.48H156.6C156.6,90.48 151.8,149.84 150.83,161.88C150.78,162.43 150.97,162.97 151.34,163.37C151.71,163.77 152.23,164 152.78,164H165.22C165.76,164 166.29,163.77 166.65,163.37C167.03,162.97 167.21,162.44 167.18,161.9C166.3,149.88 161.97,90.48 161.97,90.48Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M77,22.85C77,22.85 98.82,24.71 119.36,26.45C139.86,28.19 156.12,44.45 157.86,64.95C159.3,81.84 160.8,99.6 161.3,105.37C161.34,105.86 161.16,106.33 160.82,106.67C160.48,107.01 160.01,107.19 159.52,107.15C153.75,106.65 135.99,105.15 119.1,103.71C98.6,101.97 82.34,85.71 80.6,65.21C78.85,44.67 77,22.85 77,22.85Z"
android:fillColor="#60A625"
android:fillType="evenOdd"/>
<path
android:pathData="M241,22.85C241,22.85 239.15,44.67 237.4,65.21C235.66,85.71 219.4,101.97 198.9,103.71C182.01,105.15 164.26,106.66 158.48,107.15C158,107.19 157.52,107.01 157.18,106.67C156.84,106.33 156.66,105.86 156.7,105.37C157.2,99.6 158.71,81.84 160.14,64.95C161.88,44.45 178.14,28.19 198.64,26.45C219.18,24.71 241,22.85 241,22.85Z"
android:fillColor="#7EC046"
android:fillType="evenOdd"/>
</group>
</group>
</vector>
================================================
FILE: app/src/main/res/drawable/local_fire_department_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6,14q0,1.3 0.525,2.462 0.525,1.163 1.5,2.038Q8,18.375 8,18.275v-0.225q0,-0.8 0.3,-1.5t0.875,-1.275L12,12.5l2.825,2.775q0.575,0.575 0.875,1.275 0.3,0.7 0.3,1.5v0.225q0,0.1 -0.025,0.225 0.975,-0.875 1.5,-2.038Q18,15.3 18,14q0,-1.25 -0.462,-2.363 -0.463,-1.112 -1.338,-1.987 -0.5,0.325 -1.05,0.487 -0.55,0.163 -1.125,0.163 -1.55,0 -2.687,-1.025Q10.2,8.25 10.025,6.75 9.05,7.575 8.3,8.462q-0.75,0.888 -1.262,1.8 -0.513,0.913 -0.775,1.863Q6,13.075 6,14ZM12,15.3 L10.575,16.7q-0.275,0.275 -0.425,0.625 -0.15,0.35 -0.15,0.725 0,0.8 0.588,1.375Q11.175,20 12,20t1.413,-0.575Q14,18.85 14,18.05q0,-0.4 -0.15,-0.738 -0.15,-0.337 -0.425,-0.612ZM12,3v3.3q0,0.85 0.588,1.425 0.587,0.575 1.437,0.575 0.45,0 0.838,-0.187 0.387,-0.188 0.687,-0.563L16,7q1.85,1.05 2.925,2.925Q20,11.8 20,14q0,3.35 -2.325,5.675Q15.35,22 12,22q-3.35,0 -5.675,-2.325Q4,17.35 4,14q0,-3.225 2.163,-6.125Q8.325,4.975 12,3Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/nature_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5,22v-2h6v-4L9,16q-2.075,0 -3.537,-1.463Q4,13.075 4,11q0,-1.5 0.825,-2.763Q5.65,6.975 7.05,6.4q0.225,-1.875 1.638,-3.138Q10.1,2 12,2t3.312,1.262Q16.725,4.525 16.95,6.4q1.4,0.575 2.225,1.837Q20,9.5 20,11q0,2.075 -1.462,3.537Q17.075,16 15,16h-2v4h6v2ZM9,14h6q1.25,0 2.125,-0.875T18,11q0,-0.9 -0.512,-1.65 -0.513,-0.75 -1.338,-1.1L15.1,7.8l-0.15,-1.15q-0.15,-1.125 -0.987,-1.888Q13.125,4 12,4t-1.962,0.762Q9.2,5.525 9.05,6.65L8.9,7.8l-1.05,0.45q-0.825,0.35 -1.337,1.1Q6,10.1 6,11q0,1.25 0.875,2.125T9,14ZM12,9Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/shape_chart_bar.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:tint="?attr/colorSurfaceVariant">
<corners
android:topLeftRadius="4dp"
android:topRightRadius="4dp" />
</shape>
================================================
FILE: app/src/main/res/drawable/shape_circle.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:tint="?attr/colorSurface">
<corners android:radius="999dp" />
</shape>
================================================
FILE: app/src/main/res/drawable/shape_divider.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="80dp">
<shape>
<size android:height="1dp" />
<solid android:color="?attr/colorOutlineVariant" />
</shape>
</inset>
================================================
FILE: app/src/main/res/drawable/shape_ground.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/colorSecondaryContainer" />
<corners
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
</shape>
================================================
FILE: app/src/main/res/drawable/show_chart_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M3.5,18.5 L2,17l7.5,-7.5 4,4 7.1,-8L22,6.9l-8.5,9.6 -4,-4Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/stage_1.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="500dp"
android:height="500dp"
android:viewportWidth="500"
android:viewportHeight="500">
<group>
<clip-path
android:pathData="M91,452h319v48h-319z"/>
<path
android:pathData="M250.5,500C338.59,500 410,489.26 410,476C410,462.74 338.59,452 250.5,452C162.41,452 91,462.74 91,476C91,489.26 162.41,500 250.5,500Z"
android:fillColor="#60A625"/>
<path
android:pathData="M250.5,485C283.64,485 310.5,480.97 310.5,476C310.5,471.03 283.64,467 250.5,467C217.36,467 190.5,471.03 190.5,476C190.5,480.97 217.36,485 250.5,485Z"
android:fillColor="#51901C"/>
</group>
<group>
<clip-path
android:pathData="M168,312h164v164h-164z"/>
<path
android:pathData="M252.97,402.48H247.6C247.6,402.48 242.8,461.84 241.83,473.88C241.78,474.42 241.97,474.97 242.34,475.37C242.71,475.77 243.23,476 243.78,476H256.22C256.77,476 257.29,475.77 257.65,475.37C258.03,474.97 258.21,474.44 258.18,473.9C257.3,461.88 252.97,402.48 252.97,402.48Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M168,334.85C168,334.85 189.82,336.7 210.36,338.45C230.86,340.19 247.12,356.45 248.86,376.95C250.3,393.84 251.8,411.6 252.3,417.37C252.34,417.86 252.16,418.33 251.82,418.67C251.48,419.01 251.01,419.19 250.52,419.15C244.75,418.65 226.99,417.15 210.1,415.71C189.6,413.97 173.34,397.71 171.6,377.21C169.85,356.67 168,334.85 168,334.85Z"
android:fillColor="#60A625"
android:fillType="evenOdd"/>
<path
android:pathData="M332,334.85C332,334.85 330.15,356.67 328.4,377.21C326.66,397.71 310.4,413.97 289.9,415.71C273.01,417.15 255.26,418.66 249.48,419.15C249,419.19 248.52,419.01 248.18,418.67C247.84,418.33 247.66,417.86 247.7,417.37C248.2,411.6 249.71,393.84 251.14,376.95C252.88,356.45 269.14,340.19 289.64,338.45C310.18,336.71 332,334.85 332,334.85Z"
android:fillColor="#7EC046"
android:fillType="evenOdd"/>
</group>
</vector>
================================================
FILE: app/src/main/res/drawable/stage_2.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="500dp"
android:height="500dp"
android:viewportWidth="500"
android:viewportHeight="500">
<group>
<clip-path
android:pathData="M91,452h319v48h-319z"/>
<path
android:pathData="M250.5,500C338.59,500 410,489.26 410,476C410,462.74 338.59,452 250.5,452C162.41,452 91,462.74 91,476C91,489.26 162.41,500 250.5,500Z"
android:fillColor="#60A625"/>
<path
android:pathData="M250.5,485C283.64,485 310.5,480.97 310.5,476C310.5,471.03 283.64,467 250.5,467C217.36,467 190.5,471.03 190.5,476C190.5,480.97 217.36,485 250.5,485Z"
android:fillColor="#51901C"/>
</group>
<group>
<clip-path
android:pathData="M125,226h250v250h-250z"/>
<path
android:pathData="M254.29,369.73H246.53C246.53,369.73 239.59,455.53 238.18,472.93C238.12,473.72 238.39,474.51 238.93,475.08C239.46,475.67 240.22,476 241.01,476H258.99C259.78,476 260.53,475.67 261.07,475.09C261.6,474.52 261.87,473.74 261.82,472.96C260.55,455.6 254.29,369.73 254.29,369.73Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M229.17,400.21L226.13,402.61C226.13,402.61 241.04,427.14 244.07,432.12C244.2,432.34 244.47,432.46 244.8,432.45C245.13,432.43 245.49,432.29 245.8,432.04L252.87,426.48C253.18,426.23 253.4,425.92 253.49,425.6C253.59,425.28 253.54,425 253.35,424.81C249.28,420.67 229.17,400.21 229.17,400.21Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M276.03,383.09L273.29,380.35C273.29,380.35 250.69,398.05 246.1,401.64C245.89,401.8 245.8,402.08 245.86,402.4C245.91,402.73 246.1,403.08 246.38,403.36L252.74,409.71C253.02,409.99 253.36,410.18 253.68,410.23C254.01,410.29 254.29,410.2 254.45,410C258.08,405.47 276.03,383.09 276.03,383.09Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M138.63,315.07C138.63,315.07 155.78,316.52 175.16,318.17C206.41,320.83 231.2,345.61 233.85,376.86C235.09,391.42 236.22,404.73 236.71,410.44C236.77,411.18 236.51,411.9 235.99,412.42C235.46,412.94 234.74,413.21 234.01,413.15C228.29,412.66 214.99,411.53 200.43,410.29C169.18,407.64 144.39,382.85 141.73,351.6C140.09,332.22 138.63,315.07 138.63,315.07Z"
android:fillColor="#60A625"
android:fillType="evenOdd"/>
<path
android:pathData="M239.54,227.93C239.54,227.93 254.64,243.27 270.73,259.62C292.73,281.98 295.42,316.93 277.1,342.39C266.67,356.88 256.75,370.66 252.86,376.08C252.43,376.67 251.76,377.05 251.02,377.11C250.29,377.16 249.56,376.89 249.05,376.37C244.37,371.62 232.46,359.51 219.93,346.79C197.93,324.43 195.24,289.48 213.56,264.02C226.97,245.4 239.54,227.93 239.54,227.93Z"
android:fillColor="#7EC046"
android:fillType="evenOdd"/>
<path
android:pathData="M375,281.69C375,281.69 373.18,303.13 371.23,326C368.58,357.25 343.79,382.04 312.54,384.7C294.75,386.21 277.82,387.65 271.18,388.21C270.45,388.27 269.72,388.01 269.2,387.49C268.68,386.97 268.42,386.24 268.48,385.51C269.04,378.87 270.48,361.94 271.99,344.15C274.65,312.9 299.44,288.11 330.69,285.46C353.56,283.51 375,281.69 375,281.69Z"
android:fillColor="#6BA43C"
android:fillType="evenOdd"/>
</group>
</vector>
================================================
FILE: app/src/main/res/drawable/stage_3.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="500dp"
android:height="500dp"
android:viewportWidth="500"
android:viewportHeight="500">
<group>
<clip-path
android:pathData="M91,452h319v48h-319z"/>
<path
android:pathData="M250.5,500C338.59,500 410,489.26 410,476C410,462.74 338.59,452 250.5,452C162.41,452 91,462.74 91,476C91,489.26 162.41,500 250.5,500Z"
android:fillColor="#60A625"/>
<path
android:pathData="M250.5,485C283.64,485 310.5,480.97 310.5,476C310.5,471.03 283.64,467 250.5,467C217.36,467 190.5,471.03 190.5,476C190.5,480.97 217.36,485 250.5,485Z"
android:fillColor="#51901C"/>
</group>
<group>
<clip-path
android:pathData="M79,134h342v342h-342z"/>
<path
android:pathData="M257.44,172.57H243.99C243.99,172.57 231.95,417.55 229.5,467.25C229.4,469.5 229.86,471.73 230.79,473.39C231.72,475.05 233.03,476 234.4,476H265.59C266.96,476 268.27,475.07 269.19,473.41C270.13,471.77 270.6,469.56 270.5,467.33C268.3,417.74 257.44,172.57 257.44,172.57Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M218.93,335.34L213.62,338.03C213.62,338.03 232.19,386.35 235.96,396.15C236.13,396.6 236.53,396.92 237.05,397.05C237.58,397.17 238.18,397.09 238.73,396.81L251.03,390.56C251.57,390.29 252,389.85 252.2,389.35C252.41,388.86 252.39,388.35 252.14,387.95C246.55,379.1 218.93,335.34 218.93,335.34Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M333.21,309.07L328.44,305.51C328.44,305.51 266.02,380.17 253.36,395.31C252.79,396 252.42,396.83 252.36,397.61C252.29,398.38 252.53,399.03 253.02,399.39L264.08,407.65C264.56,408.01 265.24,408.06 265.97,407.78C266.69,407.51 267.38,406.93 267.87,406.19C278.86,389.86 333.21,309.07 333.21,309.07Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M241.67,138.94C213.53,149.71 176.44,159.61 162.27,217.41C148.1,275.21 176.01,293.32 191.12,302.77C204.82,311.34 293.97,344.73 331.16,268.75C371.28,186.8 354.54,172.39 331.13,152.81C309.65,134.84 268.38,128.72 241.67,138.94Z"
android:fillColor="#6DAC38"
android:fillType="evenOdd"/>
<path
android:pathData="M372.24,275.46C372.24,275.46 372.29,275.61 372.39,275.9C379.62,297.98 373.82,322.25 357.4,338.68C340.97,355.11 316.7,360.9 294.62,353.67L294.62,353.67C294.34,353.58 294.12,353.36 294.03,353.08L294.03,353.08C286.8,331.01 292.6,306.74 309.02,290.31C325.45,273.88 349.72,268.08 371.8,275.32C372.09,275.41 372.24,275.46 372.24,275.46Z"
android:fillColor="#5B8F30"
android:fillType="evenOdd"/>
<path
android:pathData="M205.1,263.3C206.3,263.3 207.28,262.33 207.28,261.12C207.28,259.92 206.3,258.94 205.1,258.94C203.89,258.94 202.92,259.92 202.92,261.12C202.92,262.33 203.89,263.3 205.1,263.3Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M253.02,187.16C253.81,187.16 254.44,186.53 254.44,185.74C254.44,184.96 253.81,184.32 253.02,184.32C252.24,184.32 251.6,184.96 251.6,185.74C251.6,186.53 252.24,187.16 253.02,187.16Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M224.59,212.31C225.16,212.31 225.63,211.85 225.63,211.28C225.63,210.71 225.16,210.25 224.59,210.25C224.02,210.25 223.56,210.71 223.56,211.28C223.56,211.85 224.02,212.31 224.59,212.31Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M196.86,242.46C197.69,242.46 198.36,241.78 198.36,240.95C198.36,240.12 197.69,239.45 196.86,239.45C196.02,239.45 195.35,240.12 195.35,240.95C195.35,241.78 196.02,242.46 196.86,242.46Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M209.6,179.43C212.02,179.43 213.97,177.48 213.97,175.07C213.97,172.65 212.02,170.7 209.6,170.7C207.19,170.7 205.24,172.65 205.24,175.07C205.24,177.48 207.19,179.43 209.6,179.43Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M285.17,247.47C286.56,247.47 287.68,246.35 287.68,244.96C287.68,243.58 286.56,242.46 285.17,242.46C283.79,242.46 282.67,243.58 282.67,244.96C282.67,246.35 283.79,247.47 285.17,247.47Z"
android:fillColor="#9FE664"/>
</group>
</vector>
================================================
FILE: app/src/main/res/drawable/stage_4.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="500dp"
android:height="500dp"
android:viewportWidth="500"
android:viewportHeight="500">
<group>
<clip-path
android:pathData="M91,452h319v48h-319z"/>
<path
android:pathData="M250.5,500C338.59,500 410,489.26 410,476C410,462.74 338.59,452 250.5,452C162.41,452 91,462.74 91,476C91,489.26 162.41,500 250.5,500Z"
android:fillColor="#60A625"/>
<path
android:pathData="M250.5,485C283.64,485 310.5,480.97 310.5,476C310.5,471.03 283.64,467 250.5,467C217.36,467 190.5,471.03 190.5,476C190.5,480.97 217.36,485 250.5,485Z"
android:fillColor="#51901C"/>
</group>
<group>
<clip-path
android:pathData="M68,78h398v398h-398z"/>
<path
android:pathData="M259.78,135.64H245.12C245.12,135.64 231.99,410.43 229.32,466.18C229.21,468.71 229.71,471.21 230.73,473.07C231.75,474.94 233.17,476 234.67,476H268.68C270.17,476 271.59,474.95 272.6,473.1C273.62,471.25 274.13,468.77 274.03,466.27C271.63,410.65 259.78,135.64 259.78,135.64Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M202.36,289.3L199.4,291.63C199.4,291.63 234.8,341.99 241.98,352.21C242.3,352.67 242.75,353.03 243.21,353.2C243.68,353.37 244.11,353.33 244.41,353.09L251.27,347.68C251.57,347.45 251.71,347.04 251.66,346.55C251.61,346.06 251.37,345.55 251,345.13C242.82,335.73 202.36,289.3 202.36,289.3Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M328.43,352.78L320.51,344.85C320.51,344.85 255.15,396.02 241.89,406.4C241.29,406.87 241.04,407.67 241.19,408.62C241.34,409.56 241.89,410.56 242.7,411.37L261.08,429.74C261.88,430.55 262.88,431.1 263.82,431.25C264.76,431.41 265.56,431.16 266.03,430.57C276.53,417.48 328.43,352.78 328.43,352.78Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M347.53,175C336.88,142.43 327.52,99.53 264.8,82.73C202.07,65.92 181.12,98 170.16,115.36C160.22,131.11 120.06,233.77 201.76,277.29C289.9,324.24 306.35,305.02 328.73,278.16C349.28,253.5 357.65,205.91 347.53,175Z"
android:fillColor="#6DAC38"
android:fillType="evenOdd"/>
<path
android:pathData="M380.39,336.61C375.19,320.72 370.63,299.8 340.03,291.6C309.44,283.41 299.22,299.05 293.88,307.52C289.03,315.2 269.44,365.27 309.29,386.5C352.27,409.39 360.3,400.02 371.21,386.92C381.23,374.89 385.32,351.68 380.39,336.61Z"
android:fillColor="#507E28"
android:fillType="evenOdd"/>
<path
android:pathData="M203.89,216.17C205.29,216.17 206.42,215.03 206.42,213.63C206.42,212.23 205.29,211.1 203.89,211.1C202.49,211.1 201.35,212.23 201.35,213.63C201.35,215.03 202.49,216.17 203.89,216.17Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M259.66,127.56C260.57,127.56 261.31,126.82 261.31,125.91C261.31,124.99 260.57,124.25 259.66,124.25C258.74,124.25 258,124.99 258,125.91C258,126.82 258.74,127.56 259.66,127.56Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M226.57,156.83C227.24,156.83 227.77,156.29 227.77,155.63C227.77,154.97 227.24,154.43 226.57,154.43C225.91,154.43 225.38,154.97 225.38,155.63C225.38,156.29 225.91,156.83 226.57,156.83Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M194.29,191.91C195.26,191.91 196.04,191.13 196.04,190.16C196.04,189.19 195.26,188.41 194.29,188.41C193.33,188.41 192.54,189.19 192.54,190.16C192.54,191.13 193.33,191.91 194.29,191.91Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M209.13,118.57C211.94,118.57 214.21,116.29 214.21,113.48C214.21,110.68 211.94,108.4 209.13,108.4C206.32,108.4 204.05,110.68 204.05,113.48C204.05,116.29 206.32,118.57 209.13,118.57Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M297.07,197.74C298.68,197.74 299.99,196.44 299.99,194.82C299.99,193.21 298.68,191.91 297.07,191.91C295.46,191.91 294.16,193.21 294.16,194.82C294.16,196.44 295.46,197.74 297.07,197.74Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M354.31,322.43C355.28,322.43 356.07,321.64 356.07,320.66C356.07,319.69 355.28,318.9 354.31,318.9C353.33,318.9 352.55,319.69 352.55,320.66C352.55,321.64 353.33,322.43 354.31,322.43Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M372.08,358.76C373.68,358.76 374.98,357.47 374.98,355.86C374.98,354.26 373.68,352.96 372.08,352.96C370.48,352.96 369.18,354.26 369.18,355.86C369.18,357.47 370.48,358.76 372.08,358.76Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M353.43,352.96C353.91,352.96 354.31,352.57 354.31,352.08C354.31,351.6 353.91,351.2 353.43,351.2C352.94,351.2 352.55,351.6 352.55,352.08C352.55,352.57 352.94,352.96 353.43,352.96Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M340.95,381C342.96,381 344.58,379.37 344.58,377.36C344.58,375.35 342.96,373.72 340.95,373.72C338.94,373.72 337.31,375.35 337.31,377.36C337.31,379.37 338.94,381 340.95,381Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M332.9,339.2C333.42,339.2 333.84,338.78 333.84,338.26C333.84,337.75 333.42,337.33 332.9,337.33C332.38,337.33 331.96,337.75 331.96,338.26C331.96,338.78 332.38,339.2 332.9,339.2Z"
android:fillColor="#71BA33"/>
</group>
</vector>
================================================
FILE: app/src/main/res/drawable/stage_5.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="500dp"
android:height="500dp"
android:viewportWidth="500"
android:viewportHeight="500">
<group>
<clip-path
android:pathData="M91,452h319v48h-319z"/>
<path
android:pathData="M250.5,500C338.59,500 410,489.26 410,476C410,462.74 338.59,452 250.5,452C162.41,452 91,462.74 91,476C91,489.26 162.41,500 250.5,500Z"
android:fillColor="#60A625"/>
<path
android:pathData="M250.5,485C283.64,485 310.5,480.97 310.5,476C310.5,471.03 283.64,467 250.5,467C217.36,467 190.5,471.03 190.5,476C190.5,480.97 217.36,485 250.5,485Z"
android:fillColor="#51901C"/>
</group>
<group>
<clip-path
android:pathData="M30,35h441v441h-441z"/>
<path
android:pathData="M258.45,227.57H244.07C244.07,227.57 231.19,428.14 228.58,468.83C228.46,470.68 228.96,472.51 229.95,473.86C230.95,475.23 232.35,476 233.82,476H267.18C268.64,476 270.04,475.24 271.03,473.88C272.03,472.53 272.53,470.72 272.43,468.9C270.08,428.3 258.45,227.57 258.45,227.57Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M207.76,303.24L201.33,308.3C201.33,308.3 232.8,360.07 239.19,370.57C239.48,371.05 240.04,371.3 240.74,371.27C241.43,371.24 242.2,370.93 242.86,370.41L257.76,358.67C258.41,358.15 258.9,357.48 259.09,356.82C259.28,356.15 259.17,355.54 258.79,355.15C250.2,346.42 207.76,303.24 207.76,303.24Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M309.46,285.38L303.67,279.6C303.67,279.6 255.97,316.94 246.29,324.52C245.86,324.86 245.67,325.45 245.78,326.14C245.89,326.83 246.29,327.55 246.88,328.14L260.3,341.56C260.89,342.15 261.61,342.55 262.3,342.66C262.98,342.77 263.57,342.59 263.91,342.16C271.58,332.61 309.46,285.38 309.46,285.38Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M182.14,184.93C153.03,179.96 116.9,169.78 75.06,211.62C33.21,253.47 47.13,282.77 54.85,298.34C61.84,312.46 119,385.37 189.52,339.65C265.58,290.35 259.07,269.76 249.68,241.47C241.06,215.5 209.76,189.65 182.14,184.93Z"
android:fillColor="#4A8D12"
android:fillType="evenOdd"/>
<path
android:pathData="M355.48,142.7C343.65,106.53 333.27,58.91 263.62,40.25C193.98,21.59 170.72,57.2 158.55,76.48C147.51,93.97 102.92,207.95 193.64,256.27C291.49,308.4 309.76,287.06 334.61,257.24C357.42,229.86 366.71,177.02 355.48,142.7Z"
android:fillColor="#6DAC38"
android:fillType="evenOdd"/>
<path
android:pathData="M348.73,224.19C333.49,221.59 314.57,216.26 292.67,238.16C270.76,260.07 278.05,275.42 282.08,283.56C285.75,290.95 315.67,329.13 352.59,305.2C392.42,279.38 389.01,268.6 384.09,253.79C379.58,240.2 363.19,226.66 348.73,224.19Z"
android:fillColor="#4A8D12"
android:fillType="evenOdd"/>
<path
android:pathData="M180.57,188.09C182.12,188.09 183.38,186.84 183.38,185.29C183.38,183.73 182.12,182.48 180.57,182.48C179.02,182.48 177.76,183.73 177.76,185.29C177.76,186.84 179.02,188.09 180.57,188.09Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M242.37,89.92C243.38,89.92 244.2,89.1 244.2,88.08C244.2,87.07 243.38,86.25 242.37,86.25C241.35,86.25 240.53,87.07 240.53,88.08C240.53,89.1 241.35,89.92 242.37,89.92Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M205.71,122.35C206.44,122.35 207.04,121.75 207.04,121.02C207.04,120.28 206.44,119.69 205.71,119.69C204.97,119.69 204.38,120.28 204.38,121.02C204.38,121.75 204.97,122.35 205.71,122.35Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M169.94,161.22C171.01,161.22 171.88,160.35 171.88,159.28C171.88,158.21 171.01,157.34 169.94,157.34C168.87,157.34 168,158.21 168,159.28C168,160.35 168.87,161.22 169.94,161.22Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M186.38,79.95C189.49,79.95 192.01,77.43 192.01,74.32C192.01,71.21 189.49,68.69 186.38,68.69C183.27,68.69 180.75,71.21 180.75,74.32C180.75,77.43 183.27,79.95 186.38,79.95Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M283.82,167.68C285.61,167.68 287.05,166.23 287.05,164.45C287.05,162.66 285.61,161.22 283.82,161.22C282.04,161.22 280.59,162.66 280.59,164.45C280.59,166.23 282.04,167.68 283.82,167.68Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M342.64,242.21C343.72,242.21 344.6,241.33 344.6,240.25C344.6,239.17 343.72,238.3 342.64,238.3C341.56,238.3 340.69,239.17 340.69,240.25C340.69,241.33 341.56,242.21 342.64,242.21Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M362.33,282.47C364.1,282.47 365.54,281.03 365.54,279.26C365.54,277.48 364.1,276.04 362.33,276.04C360.55,276.04 359.12,277.48 359.12,279.26C359.12,281.03 360.55,282.47 362.33,282.47Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M341.67,276.04C342.2,276.04 342.64,275.6 342.64,275.07C342.64,274.53 342.2,274.09 341.67,274.09C341.13,274.09 340.69,274.53 340.69,275.07C340.69,275.6 341.13,276.04 341.67,276.04Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M327.83,307.11C330.06,307.11 331.87,305.3 331.87,303.07C331.87,300.85 330.06,299.04 327.83,299.04C325.61,299.04 323.8,300.85 323.8,303.07C323.8,305.3 325.61,307.11 327.83,307.11Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M318.92,260.79C319.49,260.79 319.96,260.33 319.96,259.75C319.96,259.18 319.49,258.71 318.92,258.71C318.35,258.71 317.88,259.18 317.88,259.75C317.88,260.33 318.35,260.79 318.92,260.79Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M105.63,209.76C106.81,209.76 107.76,208.81 107.76,207.63C107.76,206.45 106.81,205.49 105.63,205.49C104.45,205.49 103.49,206.45 103.49,207.63C103.49,208.81 104.45,209.76 105.63,209.76Z"
android:fillColor="#69B528"/>
<path
android:pathData="M99.16,290.48C99.96,290.48 100.61,289.83 100.61,289.04C100.61,288.24 99.96,287.59 99.16,287.59C98.36,287.59 97.72,288.24 97.72,289.04C97.72,289.83 98.36,290.48 99.16,290.48Z"
android:fillColor="#69B528"/>
<path
android:pathData="M118.32,234.27C119.11,234.27 119.76,233.63 119.76,232.83C119.76,232.03 119.11,231.38 118.32,231.38C117.52,231.38 116.87,232.03 116.87,232.83C116.87,233.63 117.52,234.27 118.32,234.27Z"
android:fillColor="#69B528"/>
<path
android:pathData="M70.27,279.18C71.07,279.18 71.72,278.53 71.72,277.73C71.72,276.93 71.07,276.29 70.27,276.29C69.47,276.29 68.83,276.93 68.83,277.73C68.83,278.53 69.47,279.18 70.27,279.18Z"
android:fillColor="#69B528"/>
<path
android:pathData="M132.58,328.43C135.89,328.43 138.58,325.75 138.58,322.43C138.58,319.11 135.89,316.43 132.58,316.43C129.26,316.43 126.57,319.11 126.57,322.43C126.57,325.75 129.26,328.43 132.58,328.43Z"
android:fillColor="#69B528"/>
<path
android:pathData="M87.1,247.46C88.7,247.46 89.99,246.17 89.99,244.57C89.99,242.98 88.7,241.68 87.1,241.68C85.51,241.68 84.21,242.98 84.21,244.57C84.21,246.17 85.51,247.46 87.1,247.46Z"
android:fillColor="#69B528"/>
<path
android:pathData="M83.31,318.59C84.51,318.59 85.48,317.62 85.48,316.42C85.48,315.23 84.51,314.26 83.31,314.26C82.12,314.26 81.15,315.23 81.15,316.42C81.15,317.62 82.12,318.59 83.31,318.59Z"
android:fillColor="#69B528"/>
</group>
</vector>
================================================
FILE: app/src/main/res/drawable/stage_6.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="500dp"
android:height="500dp"
android:viewportWidth="500"
android:viewportHeight="500">
<group>
<clip-path
android:pathData="M91,452h319v48h-319z"/>
<path
android:pathData="M250.5,500C338.59,500 410,489.26 410,476C410,462.74 338.59,452 250.5,452C162.41,452 91,462.74 91,476C91,489.26 162.41,500 250.5,500Z"
android:fillColor="#60A625"/>
<path
android:pathData="M250.5,485C283.64,485 310.5,480.97 310.5,476C310.5,471.03 283.64,467 250.5,467C217.36,467 190.5,471.03 190.5,476C190.5,480.97 217.36,485 250.5,485Z"
android:fillColor="#51901C"/>
</group>
<group>
<clip-path
android:pathData="M25,24h452v452h-452z"/>
<path
android:pathData="M259.23,271.99H244.35C244.35,271.99 231.02,436.7 228.32,470.12C228.2,471.63 228.71,473.13 229.74,474.25C230.77,475.36 232.22,476 233.74,476H268.26C269.77,476 271.22,475.38 272.24,474.26C273.27,473.15 273.8,471.67 273.69,470.17C271.26,436.83 259.23,271.99 259.23,271.99Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M207.63,318.84L201.79,323.44C201.79,323.44 230.41,370.54 236.22,380.09C236.49,380.52 237,380.75 237.63,380.73C238.26,380.7 238.96,380.42 239.56,379.95L253.12,369.27C253.71,368.8 254.15,368.19 254.32,367.58C254.5,366.97 254.4,366.42 254.05,366.06C246.24,358.12 207.63,318.84 207.63,318.84Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M302.94,291.44L297.68,286.17C297.68,286.17 254.28,320.15 245.48,327.04C245.08,327.35 244.91,327.89 245.01,328.51C245.11,329.14 245.48,329.8 246.01,330.34L258.22,342.54C258.75,343.08 259.41,343.44 260.03,343.54C260.66,343.65 261.19,343.48 261.51,343.09C268.48,334.4 302.94,291.44 302.94,291.44Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M93.98,156.1C76.35,184.45 50.45,217.35 74.4,286.03C98.35,354.71 133.87,356.5 152.87,357.11C170.11,357.66 268.24,337.99 252.83,236.39C236.2,126.8 212.67,122.04 180.14,115.97C150.27,110.4 110.71,129.2 93.98,156.1Z"
android:fillColor="#3F6B19"
android:fillType="evenOdd"/>
<path
android:pathData="M258.49,156.1C240.86,184.45 214.96,217.35 238.91,286.03C262.86,354.71 298.38,356.5 317.38,357.11C334.61,357.66 432.74,337.99 417.33,236.39C400.71,126.8 377.18,122.04 344.65,115.97C314.78,110.4 275.21,129.2 258.49,156.1Z"
android:fillColor="#7EC046"
android:fillType="evenOdd"/>
<path
android:pathData="M294.79,30.18C257.35,23.78 210.86,10.69 157.03,64.52C103.2,118.35 121.11,156.05 131.03,176.08C140.03,194.24 213.56,288.04 304.28,229.23C402.14,165.8 393.76,139.31 381.68,102.92C370.59,69.51 330.33,36.26 294.79,30.18Z"
android:fillColor="#60A625"
android:fillType="evenOdd"/>
<path
android:pathData="M299.72,289.93L294.02,285.15C294.02,285.15 253.76,322.79 245.6,330.42C245.23,330.77 245.1,331.32 245.26,331.93C245.42,332.55 245.84,333.18 246.42,333.66L259.64,344.75C260.22,345.24 260.91,345.54 261.54,345.59C262.17,345.64 262.69,345.43 262.97,345.01C269.15,335.74 299.72,289.93 299.72,289.93Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M352.44,161.09C325.97,156.56 293.1,147.3 255.03,185.37C216.97,223.43 229.63,250.09 236.65,264.25C243.01,277.1 295.01,343.42 359.16,301.84C428.35,256.98 422.43,238.26 413.89,212.52C406.04,188.9 377.57,165.38 352.44,161.09Z"
android:fillColor="#4A8D12"
android:fillType="evenOdd"/>
<path
android:pathData="M253.73,385.73C258.45,385.73 262.27,380.74 262.27,374.6C262.27,368.45 258.45,363.47 253.73,363.47C249.02,363.47 245.19,368.45 245.19,374.6C245.19,380.74 249.02,385.73 253.73,385.73Z"
android:fillColor="#2B1604"/>
<path
android:pathData="M206.82,171.48C208.41,171.48 209.7,170.2 209.7,168.6C209.7,167.01 208.41,165.73 206.82,165.73C205.23,165.73 203.95,167.01 203.95,168.6C203.95,170.2 205.23,171.48 206.82,171.48Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M270.16,70.86C271.2,70.86 272.04,70.02 272.04,68.98C272.04,67.94 271.2,67.1 270.16,67.1C269.13,67.1 268.28,67.94 268.28,68.98C268.28,70.02 269.13,70.86 270.16,70.86Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M232.59,104.1C233.34,104.1 233.95,103.49 233.95,102.73C233.95,101.98 233.34,101.37 232.59,101.37C231.84,101.37 231.23,101.98 231.23,102.73C231.23,103.49 231.84,104.1 232.59,104.1Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M195.93,143.94C197.03,143.94 197.92,143.05 197.92,141.95C197.92,140.85 197.03,139.96 195.93,139.96C194.83,139.96 193.94,140.85 193.94,141.95C193.94,143.05 194.83,143.94 195.93,143.94Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M212.78,60.64C215.97,60.64 218.55,58.06 218.55,54.87C218.55,51.68 215.97,49.1 212.78,49.1C209.59,49.1 207.01,51.68 207.01,54.87C207.01,58.06 209.59,60.64 212.78,60.64Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M312.65,150.56C314.48,150.56 315.97,149.08 315.97,147.25C315.97,145.42 314.48,143.94 312.65,143.94C310.83,143.94 309.34,145.42 309.34,147.25C309.34,149.08 310.83,150.56 312.65,150.56Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M358.01,207.12C359.11,207.12 360.01,206.22 360.01,205.12C360.01,204.01 359.11,203.11 358.01,203.11C356.9,203.11 356.01,204.01 356.01,205.12C356.01,206.22 356.9,207.12 358.01,207.12Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M378.19,248.39C380.01,248.39 381.48,246.91 381.48,245.09C381.48,243.27 380.01,241.8 378.19,241.8C376.37,241.8 374.89,243.27 374.89,245.09C374.89,246.91 376.37,248.39 378.19,248.39Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M357.01,241.8C357.56,241.8 358.01,241.35 358.01,240.8C358.01,240.25 357.56,239.8 357.01,239.8C356.45,239.8 356.01,240.25 356.01,240.8C356.01,241.35 356.45,241.8 357.01,241.8Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M342.83,273.64C345.11,273.64 346.96,271.79 346.96,269.51C346.96,267.22 345.11,265.37 342.83,265.37C340.55,265.37 338.7,267.22 338.7,269.51C338.7,271.79 340.55,273.64 342.83,273.64Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M333.7,226.17C334.28,226.17 334.76,225.69 334.76,225.1C334.76,224.52 334.28,224.04 333.7,224.04C333.11,224.04 332.63,224.52 332.63,225.1C332.63,225.69 333.11,226.17 333.7,226.17Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M108.84,195.37C110,195.03 110.66,193.81 110.32,192.65C109.98,191.49 108.76,190.83 107.6,191.17C106.44,191.52 105.78,192.73 106.12,193.89C106.46,195.05 107.68,195.71 108.84,195.37Z"
android:fillColor="#69B528"/>
<path
android:pathData="M125.9,276.6C126.68,276.36 127.13,275.54 126.9,274.76C126.67,273.97 125.85,273.52 125.06,273.76C124.28,273.99 123.83,274.81 124.06,275.6C124.29,276.38 125.12,276.83 125.9,276.6Z"
android:fillColor="#69B528"/>
<path
android:pathData="M128.42,215.79C129.21,215.55 129.65,214.73 129.42,213.95C129.19,213.16 128.37,212.71 127.58,212.95C126.8,213.18 126.35,214 126.58,214.78C126.82,215.57 127.64,216.02 128.42,215.79Z"
android:fillColor="#69B528"/>
<path
android:pathData="M94.22,273.86C95.01,273.63 95.45,272.81 95.22,272.02C94.99,271.24 94.17,270.79 93.38,271.02C92.6,271.26 92.15,272.08 92.38,272.86C92.61,273.65 93.44,274.1 94.22,273.86Z"
android:fillColor="#69B528"/>
<path
android:pathData="M169.76,304.21C173.02,303.25 174.88,299.83 173.92,296.57C172.96,293.31 169.54,291.44 166.27,292.41C163.01,293.37 161.15,296.79 162.11,300.05C163.08,303.31 166.5,305.17 169.76,304.21Z"
android:fillColor="#69B528"/>
<path
android:pathData="M101.57,237.8C103.13,237.34 104.03,235.69 103.57,234.13C103.11,232.56 101.46,231.66 99.89,232.13C98.32,232.59 97.43,234.23 97.89,235.8C98.35,237.37 100,238.27 101.57,237.8Z"
android:fillColor="#69B528"/>
<path
android:pathData="M118.48,308.83C119.65,308.48 120.32,307.24 119.98,306.07C119.63,304.89 118.4,304.22 117.22,304.57C116.05,304.92 115.38,306.15 115.72,307.33C116.07,308.5 117.3,309.17 118.48,308.83Z"
android:fillColor="#69B528"/>
</group>
</vector>
================================================
FILE: app/src/main/res/drawable/steps_fill0_wght400_grad0_opsz24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5.4,9.5q0.975,0 1.85,0.35 0.875,0.35 1.6,1.025L18.4,20h0.6q0.425,0 0.712,-0.288Q20,19.425 20,19q0,-0.2 -0.038,-0.425 -0.037,-0.225 -0.262,-0.45l-4.575,-4.575L13.35,8.2l-1.85,0.45q-0.95,0.25 -1.725,-0.35Q9,7.7 9,6.725v-2.1l-0.7,-0.35 -3.85,5.15q-0.025,0.025 -0.025,0.037 0,0.013 -0.025,0.038ZM5.4,11.5L4.25,11.5q0.075,0.175 0.188,0.325 0.112,0.15 0.262,0.275l8.1,7.375q0.275,0.275 0.625,0.4t0.725,0.125h1.35l-8.025,-7.675q-0.425,-0.425 -0.962,-0.625 -0.538,-0.2 -1.113,-0.2ZM14.15,22q-0.75,0 -1.425,-0.275t-1.25,-0.775L3.35,13.575Q2.2,12.525 2.062,11q-0.137,-1.525 0.788,-2.775l3.85,-5.15q0.425,-0.575 1.138,-0.763 0.712,-0.187 1.362,0.163l0.7,0.35q0.525,0.275 0.813,0.75 0.287,0.475 0.287,1.05v2.1l1.85,-0.475q0.75,-0.2 1.45,0.188 0.7,0.387 0.95,1.112l1.625,4.9 4.25,4.25q0.5,0.5 0.687,1.075Q22,18.35 22,19q0,1.25 -0.875,2.125T19,22Z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/tree_collected.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="200dp"
android:viewportWidth="150"
android:viewportHeight="200">
<group>
<clip-path
android:pathData="M0,0h150v200h-150z"/>
<path
android:pathData="M75,200C108.14,200 135,195.97 135,191C135,186.03 108.14,182 75,182C41.86,182 15,186.03 15,191C15,195.97 41.86,200 75,200Z"
android:fillColor="#000000"
android:fillAlpha="0.1"/>
<path
android:pathData="M77.63,119.06H72.39C72.39,119.06 67.7,177 66.75,188.76C66.71,189.29 66.89,189.82 67.25,190.21C67.61,190.6 68.12,190.82 68.66,190.82H80.8C81.33,190.82 81.84,190.6 82.2,190.21C82.57,189.82 82.75,189.3 82.71,188.77C81.86,177.04 77.63,119.06 77.63,119.06Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M59.47,135.54L57.42,137.16C57.42,137.16 67.49,153.72 69.53,157.09C69.62,157.24 69.8,157.32 70.03,157.31C70.25,157.3 70.5,157.2 70.71,157.03L75.47,153.28C75.68,153.11 75.84,152.9 75.9,152.68C75.96,152.47 75.93,152.28 75.8,152.15C73.06,149.35 59.47,135.54 59.47,135.54Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M93,125.9L91.15,124.05C91.15,124.05 75.88,136 72.79,138.42C72.65,138.53 72.59,138.72 72.62,138.94C72.66,139.16 72.79,139.39 72.97,139.58L77.27,143.88C77.46,144.06 77.69,144.19 77.91,144.23C78.13,144.26 78.31,144.21 78.43,144.07C80.88,141.01 93,125.9 93,125.9Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M19.49,78.29C13.29,88.26 4.18,99.84 12.6,124C21.03,148.16 33.52,148.79 40.21,149C46.27,149.2 80.79,142.28 75.37,106.53C69.52,67.98 61.25,66.31 49.8,64.17C39.3,62.21 25.38,68.83 19.49,78.29Z"
android:fillColor="#3F6B19"
android:fillType="evenOdd"/>
<path
android:pathData="M77.36,78.29C71.16,88.26 62.05,99.84 70.48,124C78.9,148.16 91.4,148.79 98.08,149C104.14,149.2 138.66,142.28 133.24,106.53C127.4,67.98 119.12,66.31 107.67,64.17C97.17,62.21 83.25,68.83 77.36,78.29Z"
android:fillColor="#7EC046"
android:fillType="evenOdd"/>
<path
android:pathData="M90.13,33.99C76.96,31.74 60.61,27.13 41.67,46.07C22.74,65.01 29.04,78.27 32.53,85.32C35.69,91.71 61.56,124.7 93.47,104.01C127.9,81.7 124.95,72.38 120.7,59.58C116.8,47.83 102.64,36.13 90.13,33.99Z"
android:fillColor="#60A625"
android:fillType="evenOdd"/>
<path
android:pathData="M91.87,125.37L89.86,123.68C89.86,123.68 75.7,136.93 72.83,139.61C72.7,139.74 72.66,139.93 72.71,140.14C72.77,140.36 72.91,140.58 73.12,140.75L77.77,144.65C77.97,144.82 78.22,144.93 78.44,144.95C78.66,144.96 78.84,144.89 78.94,144.74C81.11,141.49 91.87,125.37 91.87,125.37Z"
android:fillColor="#413022"
android:fillType="evenOdd"/>
<path
android:pathData="M110.42,80.04C101.1,78.45 89.54,75.19 76.15,88.58C62.76,101.98 67.21,111.35 69.68,116.33C71.92,120.85 90.21,144.19 112.78,129.56C137.12,113.78 135.04,107.19 132.03,98.14C129.27,89.83 119.26,81.55 110.42,80.04Z"
android:fillColor="#4A8D12"
android:fillType="evenOdd"/>
<path
android:pathData="M75.69,159.07C77.35,159.07 78.69,157.31 78.69,155.15C78.69,152.99 77.35,151.24 75.69,151.24C74.03,151.24 72.69,152.99 72.69,155.15C72.69,157.31 74.03,159.07 75.69,159.07Z"
android:fillColor="#2B1604"/>
<path
android:pathData="M59.19,83.7C59.75,83.7 60.2,83.25 60.2,82.69C60.2,82.13 59.75,81.67 59.19,81.67C58.63,81.67 58.18,82.13 58.18,82.69C58.18,83.25 58.63,83.7 59.19,83.7Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M81.47,48.3C81.84,48.3 82.13,48.01 82.13,47.64C82.13,47.28 81.84,46.98 81.47,46.98C81.11,46.98 80.81,47.28 80.81,47.64C80.81,48.01 81.11,48.3 81.47,48.3Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M68.25,59.99C68.52,59.99 68.73,59.78 68.73,59.52C68.73,59.25 68.52,59.04 68.25,59.04C67.99,59.04 67.77,59.25 67.77,59.52C67.77,59.78 67.99,59.99 68.25,59.99Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M55.36,74.01C55.74,74.01 56.06,73.7 56.06,73.31C56.06,72.92 55.74,72.61 55.36,72.61C54.97,72.61 54.66,72.92 54.66,73.31C54.66,73.7 54.97,74.01 55.36,74.01Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M61.28,44.71C62.41,44.71 63.31,43.8 63.31,42.68C63.31,41.56 62.41,40.65 61.28,40.65C60.16,40.65 59.25,41.56 59.25,42.68C59.25,43.8 60.16,44.71 61.28,44.71Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M96.42,76.34C97.06,76.34 97.58,75.82 97.58,75.17C97.58,74.53 97.06,74.01 96.42,74.01C95.78,74.01 95.25,74.53 95.25,75.17C95.25,75.82 95.78,76.34 96.42,76.34Z"
android:fillColor="#9FE664"/>
<path
android:pathData="M112.37,96.24C112.76,96.24 113.08,95.92 113.08,95.53C113.08,95.14 112.76,94.83 112.37,94.83C111.98,94.83 111.67,95.14 111.67,95.53C111.67,95.92 111.98,96.24 112.37,96.24Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M119.47,110.75C120.11,110.75 120.63,110.24 120.63,109.6C120.63,108.96 120.11,108.44 119.47,108.44C118.83,108.44 118.31,108.96 118.31,109.6C118.31,110.24 118.83,110.75 119.47,110.75Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M112.02,108.44C112.22,108.44 112.37,108.28 112.37,108.08C112.37,107.89 112.22,107.73 112.02,107.73C111.83,107.73 111.67,107.89 111.67,108.08C111.67,108.28 111.83,108.44 112.02,108.44Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M107.03,119.64C107.84,119.64 108.49,118.99 108.49,118.18C108.49,117.38 107.84,116.73 107.03,116.73C106.23,116.73 105.58,117.38 105.58,118.18C105.58,118.99 106.23,119.64 107.03,119.64Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M103.82,102.94C104.03,102.94 104.19,102.77 104.19,102.56C104.19,102.36 104.03,102.19 103.82,102.19C103.61,102.19 103.45,102.36 103.45,102.56C103.45,102.77 103.61,102.94 103.82,102.94Z"
android:fillColor="#71BA33"/>
<path
android:pathData="M24.72,92.1C25.13,91.98 25.36,91.56 25.24,91.15C25.12,90.74 24.69,90.51 24.29,90.63C23.88,90.75 23.64,91.18 23.76,91.58C23.88,91.99 24.31,92.22 24.72,92.1Z"
android:fillColor="#69B528"/>
<path
android:pathData="M30.72,120.68C31,120.6 31.16,120.31 31.07,120.03C30.99,119.75 30.7,119.6 30.43,119.68C30.15,119.76 29.99,120.05 30.07,120.32C30.16,120.6 30.45,120.76 30.72,120.68Z"
android:fillColor="#69B528"/>
<path
android:pathData="M31.61,99.28C31.89,99.2 32.04,98.91 31.96,98.64C31.88,98.36 31.59,98.2 31.31,98.29C31.04,98.37 30.88,98.66 30.96,98.93C31.04,99.21 31.33,99.37 31.61,99.28Z"
android:fillColor="#69B528"/>
<path
android:pathData="M19.58,119.72C19.85,119.63 20.01,119.35 19.93,119.07C19.85,118.79 19.56,118.64 19.28,118.72C19.01,118.8 18.85,119.09 18.93,119.36C19.01,119.64 19.3,119.8 19.58,119.72Z"
android:fillColor="#69B528"/>
<path
android:pathData="M46.15,130.39C47.3,130.05 47.95,128.85 47.61,127.7C47.28,126.56 46.07,125.9 44.92,126.24C43.78,126.58 43.12,127.78 43.46,128.93C43.8,130.07 45,130.73 46.15,130.39Z"
android:fillColor="#69B528"/>
<path
android:pathData="M22.16,107.03C22.71,106.87 23.03,106.29 22.87,105.74C22.7,105.18 22.12,104.87 21.57,105.03C21.02,105.2 20.7,105.78 20.87,106.33C21.03,106.88 21.61,107.19 22.16,107.03Z"
android:fillColor="#69B528"/>
<path
android:pathData="M28.11,132.01C28.52,131.89 28.76,131.46 28.64,131.04C28.52,130.63 28.08,130.4 27.67,130.52C27.26,130.64 27.02,131.07 27.14,131.49C27.26,131.9 27.7,132.14 28.11,132.01Z"
android:fillColor="#69B528"/>
</group>
</vector>
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".core.presentation.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:navGraph="@navigation/nav_graph" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_navigation_menu" />
</LinearLayout>
================================================
FILE: app/src/main/res/layout/activity_onboarding.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".core.presentation.OnboardingActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/onboarding_nav_graph" />
</FrameLayout>
================================================
FILE: app/src/main/res/layout/activity_settings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".settings.SettingsActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/settings" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_fragment"
android:name="pl.bartek537.forest.settings.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
================================================
FILE: app/src/main/res/layout/fragment_activity_recognition_permission.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".core.presentation.ActivityRecognitionPermissionFragment">
<ImageView
android:id="@+id/image_walk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="38dp"
android:importantForAccessibility="no"
android:src="@drawable/directions_walk_fill0_wght400_grad0_opsz48"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/text_permission_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/allow_activity_recognition"
android:textAppearance="?attr/textAppearanceHeadline5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_walk" />
<TextView
android:id="@+id/text_permission_rationale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/activity_recognition_rationale"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_permission_title" />
<ImageView
android:id="@+id/icon_revoke_permission"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="38dp"
android:importantForAccessibility="no"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_permission_rationale"
app:srcCompat="@drawable/do_not_disturb_on_fill0_wght400_grad0_opsz24"
app:tint="?android:textColorPrimary" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/revoke_permission_rationale"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon_revoke_permission"
app:layout_constraintTop_toTopOf="@+id/icon_revoke_permission" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="@string/data_consent"
android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintBottom_toTopOf="@+id/button_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_continue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/action_continue"
app:layout_constraintBottom_toBottomOf="parent"
tools:layout_editor_absoluteX="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/fragment_forest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".trees.ForestFragment">
<TextView
android:id="@+id/text_trees_collected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textAppearance="?attr/textAppearanceDisplayMedium"
android:textColor="?attr/colorOnSurface"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="12" />
<TextView
android:id="@+id/text_trees_collected_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
app:layout_constraintBaseline_toBaselineOf="@id/text_trees_collected"
app:layout_constraintStart_toEndOf="@id/text_trees_collected"
tools:text="trees" />
<ImageView
android:id="@+id/image_ground"
android:layout_width="match_parent"
android:layout_height="64dp"
android:importantForAccessibility="no"
android:src="@drawable/shape_ground"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id='@+id/constraint_layout_trees'
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_marginBottom="-40dp"
app:layout_constraintBottom_toTopOf="@id/image_ground" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/fragment_progress.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".progress.ProgressFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/text_step_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceDisplayMedium"
android:textColor="?attr/colorOnSurface"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="3,837" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/steps"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface"
app:layout_constraintBaseline_toBaselineOf="@id/text_step_count"
app:layout_constraintStart_toEndOf="@id/text_step_count" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_daily_goal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:indicatorColor="?attr/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_step_count"
app:trackColor="?attr/colorSurfaceVariant"
app:trackCornerRadius="4dp"
app:trackThickness="8dp"
tools:max="7500"
tools:progress="3837" />
<TextView
android:id="@+id/text_daily_goal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progress_daily_goal"
tools:text="Goal 7,500" />
<ImageView
android:id="@+id/image_tree"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:maxHeight="320dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_daily_goal"
tools:src="@drawable/stage_6" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="36dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/image_tree">
<androidx.cardview.widget.CardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorTertiaryContainer"
app:cardCornerRadius="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="18dp">
<View
android:id="@+id/view_calorie_background"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/shape_circle"
android:backgroundTint="?attr/colorTertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="@+id/view_calorie_background"
app:layout_constraintEnd_toEndOf="@+id/view_calorie_background"
app:layout_constraintStart_toStartOf="@+id/view_calorie_background"
app:layout_constraintTop_toTopOf="@+id/view_calorie_background"
app:srcCompat="@drawable/local_fire_department_fill0_wght400_grad0_opsz24"
app:tint="?attr/colorOnTertiary" />
<TextView
android:id="@+id/text_calorie_burned"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="2dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintBottom_toTopOf="@+id/text_calorie_burned_label"
app:layout_constraintStart_toEndOf="@+id/view_calorie_background"
app:layout_constraintTop_toTopOf="@+id/view_calorie_background"
app:layout_constraintVertical_chainStyle="packed"
tools:text="210 kcal" />
<TextView
android:id="@+id/text_calorie_burned_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/calorie_burned"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="@+id/view_calorie_background"
app:layout_constraintStart_toStartOf="@+id/text_calorie_burned"
app:layout_constraintTop_toBottomOf="@+id/text_calorie_burned"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardCornerRadius="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="18dp">
<View
android:id="@+id/view_distance_background"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/shape_circle"
android:backgroundTint="?attr/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="@+id/view_distance_background"
app:layout_constraintEnd_toEndOf="@+id/view_distance_background"
app:layout_constraintStart_toStartOf="@+id/view_distance_background"
app:layout_constraintTop_toTopOf="@+id/view_distance_background"
app:srcCompat="@drawable/conversion_path_fill0_wght400_grad0_opsz24"
app:tint="?attr/colorOnPrimary" />
<TextView
android:id="@+id/text_distance_travelled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="2dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintBottom_toTopOf="@+id/text_distance_travelled_label"
app:layout_constraintStart_toEndOf="@+id/view_distance_background"
app:layout_constraintTop_toTopOf="@+id/view_distance_background"
app:layout_constraintVertical_chainStyle="packed"
tools:text="4.1 km" />
<TextView
android:id="@+id/text_distance_travelled_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/distance_travelled"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="@+id/view_distance_background"
app:layout_constraintStart_toStartOf="@+id/text_distance_travelled"
app:layout_constraintTop_toBottomOf="@+id/text_distance_travelled"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:cardCornerRadius="24dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="18dp">
<View
android:id="@+id/view_carbon_dioxide_background"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/shape_circle"
android:backgroundTint="?attr/colorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="@+id/view_carbon_dioxide_background"
app:layout_constraintEnd_toEndOf="@+id/view_carbon_dioxide_background"
app:layout_constraintStart_toStartOf="@+id/view_carbon_dioxide_background"
app:layout_constraintTop_toTopOf="@+id/view_carbon_dioxide_background"
app:srcCompat="@drawable/bubble_chart_fill0_wght400_grad0_opsz24"
app:tint="?attr/colorOnSecondary" />
<TextView
android:id="@+id/text_carbon_dioxide_saved"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="2dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintBottom_toTopOf="@+id/text_carbon_dioxide_saved_label"
app:layout_constraintStart_toEndOf="@+id/view_carbon_dioxide_background"
app:layout_constraintTop_toTopOf="@+id/view_carbon_dioxide_background"
app:layout_constraintVertical_chainStyle="packed"
tools:text="1.43 kg" />
<TextView
android:id="@+id/text_carbon_dioxide_saved_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/carbon_dioxide_saved"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="@+id/view_carbon_dioxide_background"
app:layout_constraintStart_toStartOf="@+id/text_carbon_dioxide_saved"
app:layout_constraintTop_toBottomOf="@+id/text_carbon_dioxide_saved"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
================================================
FILE: app/src/main/res/layout/fragment_stats.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".stats.StatsFragment">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
================================================
FILE: app/src/main/res/layout/fragment_stats_chart.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".stats.presentation.StatsChartFragment"
tools:layout_margin="24dp">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_chart"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layoutDirection="rtl"
tools:background="?attr/colorPrimaryContainer" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:minHeight="48dp">
<ImageButton
android:id="@+id/button_previous_day"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@null"
android:clickable="true"
android:contentDescription="@string/previous_day"
android:focusable="true"
android:src="@drawable/chevron_left_fill0_wght400_grad0_opsz24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnBackground" />
<TextView
android:id="@+id/text_selected_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceLabelLarge"
android:textColor="?attr/colorOnBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Fri, 12 Jun" />
<ImageButton
android:id="@+id/button_next_day"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@null"
android:clickable="true"
android:contentDescription="@string/next_day"
android:focusable="true"
android:src="@drawable/chevron_right_fill0_wght400_grad0_opsz24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnBackground" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
================================================
FILE: app/src/main/res/layout/fragment_stats_details.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="24dp"
tools:context=".stats.presentation.StatsDetailsFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_chart"
android:name="pl.bartek537.forest.stats.presentation.StatsChartFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout="@layout/fragment_stats_chart" />
<androidx.cardview.widget.CardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:cardCornerRadius="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/shape_divider"
android:orientation="vertical"
android:showDividers="middle">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="18dp">
<View
android:id="@+id/view_step_background"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/shape_circle"
android:backgroundTint="?attr/colorSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="@+id/view_step_background"
app:layout_constraintEnd_toEndOf="@+id/view_step_background"
app:layout_constraintStart_toStartOf="@+id/view_step_background"
app:layout_constraintTop_toTopOf="@+id/view_step_background"
app:srcCompat="@drawable/steps_fill0_wght400_grad0_opsz24
gitextract__9jakuws/ ├── .gitignore ├── .idea/ │ ├── .gitignore │ ├── .name │ ├── AndroidProjectSystem.xml │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── compiler.xml │ ├── deploymentTargetSelector.xml │ ├── deviceManager.xml │ ├── gradle.xml │ ├── kotlinc.xml │ ├── migrations.xml │ ├── misc.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── pl/ │ │ └── bartek537/ │ │ └── forest/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── pl/ │ │ │ └── bartek537/ │ │ │ └── forest/ │ │ │ ├── ForestApplication.kt │ │ │ ├── core/ │ │ │ │ ├── data/ │ │ │ │ │ ├── repository/ │ │ │ │ │ │ └── DayRepositoryImpl.kt │ │ │ │ │ └── source/ │ │ │ │ │ ├── DayDao.kt │ │ │ │ │ ├── ForestDatabase.kt │ │ │ │ │ └── util/ │ │ │ │ │ └── Converters.kt │ │ │ │ ├── domain/ │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── Day.kt │ │ │ │ │ │ ├── DaySettings.kt │ │ │ │ │ │ └── StatsSummary.kt │ │ │ │ │ ├── repository/ │ │ │ │ │ │ └── DayRepository.kt │ │ │ │ │ └── usecase/ │ │ │ │ │ ├── DayUseCases.kt │ │ │ │ │ ├── GetDay.kt │ │ │ │ │ ├── GetDayImpl.kt │ │ │ │ │ ├── IncrementStepCount.kt │ │ │ │ │ └── IncrementStepCountImpl.kt │ │ │ │ └── presentation/ │ │ │ │ ├── ActivityRecognitionPermissionFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── OnboardingActivity.kt │ │ │ │ └── SplashActivity.kt │ │ │ ├── progress/ │ │ │ │ ├── ProgressFragment.kt │ │ │ │ ├── ProgressState.kt │ │ │ │ └── ProgressViewModel.kt │ │ │ ├── service/ │ │ │ │ ├── StepCounterController.kt │ │ │ │ ├── StepCounterEvent.kt │ │ │ │ ├── StepCounterService.kt │ │ │ │ ├── StepCounterServiceLauncher.kt │ │ │ │ └── StepCounterState.kt │ │ │ ├── settings/ │ │ │ │ ├── SettingsActivity.kt │ │ │ │ ├── SettingsFragment.kt │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ ├── data/ │ │ │ │ │ ├── repository/ │ │ │ │ │ │ └── SettingsRepositoryImpl.kt │ │ │ │ │ └── source/ │ │ │ │ │ ├── SettingsStore.kt │ │ │ │ │ └── SettingsStoreImpl.kt │ │ │ │ └── domain/ │ │ │ │ ├── model/ │ │ │ │ │ └── Settings.kt │ │ │ │ ├── repository/ │ │ │ │ │ └── SettingsRepository.kt │ │ │ │ └── usecase/ │ │ │ │ ├── GetSettings.kt │ │ │ │ ├── SettingsUseCases.kt │ │ │ │ └── UpdateDaySettings.kt │ │ │ ├── stats/ │ │ │ │ ├── StatsFragment.kt │ │ │ │ ├── domain/ │ │ │ │ │ └── usecase/ │ │ │ │ │ ├── GetFirstDate.kt │ │ │ │ │ ├── GetSummary.kt │ │ │ │ │ ├── GetWeek.kt │ │ │ │ │ ├── StatsChartPageUseCases.kt │ │ │ │ │ ├── StatsDetailsUseCases.kt │ │ │ │ │ └── StatsSummaryUseCases.kt │ │ │ │ ├── presentation/ │ │ │ │ │ ├── ChartAdapter.kt │ │ │ │ │ ├── StatsChartFragment.kt │ │ │ │ │ ├── StatsChartPageFragment.kt │ │ │ │ │ ├── StatsChartPageViewModel.kt │ │ │ │ │ ├── StatsChartState.kt │ │ │ │ │ ├── StatsDetailsFragment.kt │ │ │ │ │ ├── StatsDetailsState.kt │ │ │ │ │ ├── StatsDetailsViewModel.kt │ │ │ │ │ ├── StatsSummaryFragment.kt │ │ │ │ │ ├── StatsSummaryState.kt │ │ │ │ │ └── StatsSummaryViewModel.kt │ │ │ │ └── util/ │ │ │ │ ├── ContextExtension.kt │ │ │ │ ├── DayExtension.kt │ │ │ │ └── LocalDateExtension.kt │ │ │ └── trees/ │ │ │ ├── ForestFragment.kt │ │ │ ├── ForestState.kt │ │ │ ├── ForestViewModel.kt │ │ │ └── domain/ │ │ │ └── usecase/ │ │ │ ├── ForestUseCases.kt │ │ │ └── GetTreeCount.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── bubble_chart_fill0_wght400_grad0_opsz24.xml │ │ │ ├── chevron_left_fill0_wght400_grad0_opsz24.xml │ │ │ ├── chevron_right_fill0_wght400_grad0_opsz24.xml │ │ │ ├── conversion_path_fill0_wght400_grad0_opsz24.xml │ │ │ ├── directions_walk_fill0_wght400_grad0_opsz48.xml │ │ │ ├── do_not_disturb_on_fill0_wght400_grad0_opsz24.xml │ │ │ ├── forest_fill0_wght400_grad0_opsz24.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── local_fire_department_fill0_wght400_grad0_opsz24.xml │ │ │ ├── nature_fill0_wght400_grad0_opsz24.xml │ │ │ ├── shape_chart_bar.xml │ │ │ ├── shape_circle.xml │ │ │ ├── shape_divider.xml │ │ │ ├── shape_ground.xml │ │ │ ├── show_chart_fill0_wght400_grad0_opsz24.xml │ │ │ ├── stage_1.xml │ │ │ ├── stage_2.xml │ │ │ ├── stage_3.xml │ │ │ ├── stage_4.xml │ │ │ ├── stage_5.xml │ │ │ ├── stage_6.xml │ │ │ ├── steps_fill0_wght400_grad0_opsz24.xml │ │ │ └── tree_collected.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── activity_onboarding.xml │ │ │ ├── activity_settings.xml │ │ │ ├── fragment_activity_recognition_permission.xml │ │ │ ├── fragment_forest.xml │ │ │ ├── fragment_progress.xml │ │ │ ├── fragment_stats.xml │ │ │ ├── fragment_stats_chart.xml │ │ │ ├── fragment_stats_details.xml │ │ │ ├── fragment_stats_page_chart.xml │ │ │ ├── fragment_stats_summary.xml │ │ │ └── item_chart_bar.xml │ │ ├── menu/ │ │ │ ├── bottom_navigation_menu.xml │ │ │ └── main_menu.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── navigation/ │ │ │ ├── nav_graph.xml │ │ │ └── onboarding_nav_graph.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── values-night/ │ │ │ └── themes.xml │ │ ├── values-v29/ │ │ │ └── themes.xml │ │ └── xml/ │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── settings.xml │ └── test/ │ └── java/ │ └── pl/ │ └── bartek537/ │ └── forest/ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle
Condensed preview — 144 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (276K chars).
[
{
"path": ".gitignore",
"chars": 225,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor."
},
{
"path": ".idea/.gitignore",
"chars": 47,
"preview": "# Default ignored files\n/shelf/\n/workspace.xml\n"
},
{
"path": ".idea/.name",
"chars": 6,
"preview": "Forest"
},
{
"path": ".idea/AndroidProjectSystem.xml",
"chars": 212,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"AndroidProjectSystem\">\n <option name="
},
{
"path": ".idea/codeStyles/Project.xml",
"chars": 5383,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <code_scheme name=\"Project\" version=\"173\">\n <JavaCodeStyleSettings"
},
{
"path": ".idea/codeStyles/codeStyleConfig.xml",
"chars": 142,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <state>\n <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
},
{
"path": ".idea/compiler.xml",
"chars": 169,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"CompilerConfiguration\">\n <bytecodeTar"
},
{
"path": ".idea/deploymentTargetSelector.xml",
"chars": 301,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"deploymentTargetSelector\">\n <selectio"
},
{
"path": ".idea/deviceManager.xml",
"chars": 351,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"DeviceTable\">\n <option name=\"columnSo"
},
{
"path": ".idea/gradle.xml",
"chars": 690,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"GradleMigrationSettings\" migrationVersio"
},
{
"path": ".idea/kotlinc.xml",
"chars": 175,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"KotlinJpsPluginSettings\">\n <option na"
},
{
"path": ".idea/migrations.xml",
"chars": 254,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ProjectMigrations\">\n <option name=\"Mi"
},
{
"path": ".idea/misc.xml",
"chars": 448,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ExternalStorageConfigurationManager\" ena"
},
{
"path": ".idea/runConfigurations.xml",
"chars": 964,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"RunConfigurationProducerService\">\n <o"
},
{
"path": ".idea/vcs.xml",
"chars": 180,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"VcsDirectoryMappings\">\n <mapping dire"
},
{
"path": "README.md",
"chars": 2782,
"preview": "# Forest\n\nTrack your daily step count, stay healthy and fight the climate change, one step at a time.\n\n"
},
{
"path": "app/src/main/java/pl/bartek537/forest/trees/ForestViewModel.kt",
"chars": 1618,
"preview": "package pl.bartek537.forest.trees\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.ViewModelProvider\nimpor"
},
{
"path": "app/src/main/java/pl/bartek537/forest/trees/domain/usecase/ForestUseCases.kt",
"chars": 245,
"preview": "package pl.bartek537.forest.trees.domain.usecase\n\nimport pl.bartek537.forest.core.domain.repository.DayRepository\n\nclass"
},
{
"path": "app/src/main/java/pl/bartek537/forest/trees/domain/usecase/GetTreeCount.kt",
"chars": 395,
"preview": "package pl.bartek537.forest.trees.domain.usecase\n\nimport kotlinx.coroutines.flow.Flow\nimport pl.bartek537.forest.core.do"
},
{
"path": "app/src/main/res/drawable/bubble_chart_fill0_wght400_grad0_opsz24.xml",
"chars": 891,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/chevron_left_fill0_wght400_grad0_opsz24.xml",
"chars": 308,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/chevron_right_fill0_wght400_grad0_opsz24.xml",
"chars": 305,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/conversion_path_fill0_wght400_grad0_opsz24.xml",
"chars": 949,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/directions_walk_fill0_wght400_grad0_opsz48.xml",
"chars": 699,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"48dp\"\n android:height=\"48dp\"\n "
},
{
"path": "app/src/main/res/drawable/do_not_disturb_on_fill0_wght400_grad0_opsz24.xml",
"chars": 747,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/forest_fill0_wght400_grad0_opsz24.xml",
"chars": 569,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 5606,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 2209,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"108dp\"\n android:height=\"108dp\"\n"
},
{
"path": "app/src/main/res/drawable/local_fire_department_fill0_wght400_grad0_opsz24.xml",
"chars": 1145,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/nature_fill0_wght400_grad0_opsz24.xml",
"chars": 772,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/shape_chart_bar.xml",
"chars": 299,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:tools"
},
{
"path": "app/src/main/res/drawable/shape_circle.xml",
"chars": 241,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:tools"
},
{
"path": "app/src/main/res/drawable/shape_divider.xml",
"chars": 266,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<inset xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:ins"
},
{
"path": "app/src/main/res/drawable/shape_ground.xml",
"chars": 265,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <solid and"
},
{
"path": "app/src/main/res/drawable/show_chart_fill0_wght400_grad0_opsz24.xml",
"chars": 322,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/stage_1.xml",
"chars": 2062,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"500dp\"\n android:height=\"500dp\"\n"
},
{
"path": "app/src/main/res/drawable/stage_2.xml",
"chars": 3340,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"500dp\"\n android:height=\"500dp\"\n"
},
{
"path": "app/src/main/res/drawable/stage_3.xml",
"chars": 4290,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"500dp\"\n android:height=\"500dp\"\n"
},
{
"path": "app/src/main/res/drawable/stage_4.xml",
"chars": 5436,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"500dp\"\n android:height=\"500dp\"\n"
},
{
"path": "app/src/main/res/drawable/stage_5.xml",
"chars": 7517,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"500dp\"\n android:height=\"500dp\"\n"
},
{
"path": "app/src/main/res/drawable/stage_6.xml",
"chars": 8505,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"500dp\"\n android:height=\"500dp\"\n"
},
{
"path": "app/src/main/res/drawable/steps_fill0_wght400_grad0_opsz24.xml",
"chars": 1098,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/tree_collected.xml",
"chars": 7813,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"150dp\"\n android:height=\"200dp\"\n"
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 1855,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/layout/activity_onboarding.xml",
"chars": 717,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns"
},
{
"path": "app/src/main/res/layout/activity_settings.xml",
"chars": 1254,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/fragment_activity_recognition_permission.xml",
"chars": 3532,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/fragment_forest.xml",
"chars": 1988,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/fragment_progress.xml",
"chars": 14631,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/"
},
{
"path": "app/src/main/res/layout/fragment_stats.xml",
"chars": 683,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/layout/fragment_stats_chart.xml",
"chars": 2780,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/layout/fragment_stats_details.xml",
"chars": 18430,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/"
},
{
"path": "app/src/main/res/layout/fragment_stats_page_chart.xml",
"chars": 900,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns"
},
{
"path": "app/src/main/res/layout/fragment_stats_summary.xml",
"chars": 19099,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android=\"http://sche"
},
{
"path": "app/src/main/res/layout/item_chart_bar.xml",
"chars": 1416,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmln"
},
{
"path": "app/src/main/res/menu/bottom_navigation_menu.xml",
"chars": 598,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <item\n "
},
{
"path": "app/src/main/res/menu/main_menu.xml",
"chars": 292,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"h"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 267,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 267,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/navigation/nav_graph.xml",
"chars": 1113,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:"
},
{
"path": "app/src/main/res/navigation/onboarding_nav_graph.xml",
"chars": 1045,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 3790,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"seed\">#646106</color>\n <color name=\"md_theme_ligh"
},
{
"path": "app/src/main/res/values/ic_launcher_background.xml",
"chars": 120,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"ic_launcher_background\">#EDEDE4</color>\n</resources>"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 3585,
"preview": "<resources>\n <string name=\"app_name\">Forest</string>\n <string name=\"step_counter_channel\">Step counter</string>\n "
},
{
"path": "app/src/main/res/values/themes.xml",
"chars": 2657,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <!-- Base application theme. -->\n <style name=\"Base.Theme.Fore"
},
{
"path": "app/src/main/res/values-night/themes.xml",
"chars": 2392,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <!-- Base application theme. -->\n <style name=\"Base.Theme.Fore"
},
{
"path": "app/src/main/res/values-v29/themes.xml",
"chars": 431,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n <style name=\"Theme.Forest\" parent=\"Base.Theme.Forest\">\n <"
},
{
"path": "app/src/main/res/xml/backup_rules.xml",
"chars": 478,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample backup rules file; uncomment and customize as necessary.\n See htt"
},
{
"path": "app/src/main/res/xml/data_extraction_rules.xml",
"chars": 551,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample data extraction rules file; uncomment and customize as necessary.\n "
},
{
"path": "app/src/main/res/xml/settings.xml",
"chars": 1500,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/test/java/pl/bartek537/forest/ExampleUnitTest.kt",
"chars": 343,
"preview": "package pl.bartek537.forest\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which wil"
},
{
"path": "build.gradle",
"chars": 164,
"preview": "plugins {\n alias libs.plugins.android.application apply false\n alias libs.plugins.kotlin.android apply false\n a"
},
{
"path": "gradle/libs.versions.toml",
"chars": 2836,
"preview": "[versions]\nactivity = \"1.11.0\"\nagp = \"8.13.0\"\nappcompat = \"1.7.1\"\nconstraintLayout = \"2.2.1\"\ncoreKtx = \"1.17.0\"\ndesugarJ"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 253,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
},
{
"path": "gradle.properties",
"chars": 1387,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "gradlew",
"chars": 8669,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
},
{
"path": "gradlew.bat",
"chars": 2776,
"preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
},
{
"path": "settings.gradle",
"chars": 323,
"preview": "pluginManagement {\n repositories {\n google()\n mavenCentral()\n gradlePluginPortal()\n }\n}\ndepen"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the bk20dev/forest GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 144 files (247.3 KB), approximately 73.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.