[
  {
    "path": ".github/ci-gradle.properties",
    "content": "#\n# Copyright 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\norg.gradle.daemon=false\norg.gradle.parallel=true\norg.gradle.workers.max=2\n\nkotlin.incremental=false\n\n# Controls KotlinOptions.allWarningsAsErrors.\n# This value used in CI and is currently set to false.\n# If you want to treat warnings as errors locally, set this property to true\n# in your ~/.gradle/gradle.properties file.\nwarningsAsErrors=false\n"
  },
  {
    "path": ".github/workflows/build_test.yaml",
    "content": "name: build_test\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    strategy:\n      matrix:\n        api-level: [29]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Enable KVM group perms\n        run: |\n          echo 'KERNEL==\"kvm\", GROUP=\"kvm\", MODE=\"0666\", OPTIONS+=\"static_node=kvm\"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules\n          sudo udevadm control --reload-rules\n          sudo udevadm trigger --name-match=kvm\n          ls /dev/kvm\n          \n      - name: Copy CI gradle.properties\n        run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties\n\n      - name: Set Up JDK\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu' # See 'Supported distributions' for available options\n          java-version: '17'\n          cache: 'gradle'\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v4\n        \n      - name: Setup Android SDK\n        uses: android-actions/setup-android@v3\n        \n      - name: Run instrumentation tests\n        uses: reactivecircus/android-emulator-runner@v2\n        with:\n          api-level: ${{ matrix.api-level }}\n          arch: x86\n          disable-animations: true\n          script: ./gradlew :app:connectedCheck --stacktrace\n\n      - name: Upload test reports\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: test-reports-${{ matrix.api-level }}\n          path: ./app/build/reports/androidTests\n"
  },
  {
    "path": ".github/workflows/copy-branch.yml",
    "content": "# Duplicates default main branch to the old master branch\n\nname: Duplicates main to old master branch\n\n# Controls when the action will run. Triggers the workflow on push or pull request\n# events but only for the main branch\non:\n  workflow_dispatch:\n  push:\n    branches: [ main ]\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n  # This workflow contains a single job called \"copy-branch\"\n  copy-branch:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it,\n    # but specifies master branch (old default).\n    - uses: actions/checkout@v4\n      with:\n        fetch-depth: 0\n        ref: master\n        \n    - run: |\n        git config user.name github-actions\n        git config user.email github-actions@github.com\n        git merge origin/main\n        git push\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\nlocal.properties\n.idea\n.DS_Store\nbuild\ncaptures\n.externalNativeBuild\n"
  },
  {
    "path": ".google/packaging.yaml",
    "content": "# Copyright (C) 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# GOOGLE SAMPLE PACKAGING DATA\n#\n# This file is used by Google as part of our samples packaging process.\n# End users may safely ignore this file. It has no relevance to other systems.\n---\nstatus:       PUBLISHED\ntechnologies: [Android, JetpackCompose, Coroutines]\ncategories:\n  - AndroidTesting\n  - AndroidArchitecture\n  - AndroidArchitectureUILayer\n  - AndroidArchitectureDataLayer\n  - AndroidArchitectureStateProduction\n  - AndroidArchitectureStateHolder\n  - AndroidArchitectureUIEvents\n  - JetpackComposeTesting\n  - JetpackComposeArchitectureAndState\n  - JetpackComposeNavigation\nlanguages:    [Kotlin]\nsolutions:\n  - Mobile\n  - Flow\n  - JetpackHilt\n  - JetpackRoom\n  - JetpackNavigation\n  - JetpackLifecycle\ngithub:       android/architecture-samples\nlevel:        INTERMEDIATE\nlicense: apache2\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "* @josealcerreca @dturner"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your patches! Before we can take them, we \nhave to jump a couple of legal hurdles.\n\n### Before you contribute\nBefore we can use your code, you must sign the\n[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)\n(CLA), which you can do online. The CLA is necessary mainly because you own the\ncopyright to your changes, even after your contribution becomes part of our\ncodebase, so we need your permission to use and distribute your code. We also\nneed to be sure of various other things—for instance that you'll tell us if you\nknow that your code infringes on other people's patents. You don't have to sign\nthe CLA until after you've submitted your code for review and a member has\napproved it, but you must do it before we can put your code into our codebase.\nBefore you start working on a larger contribution, you should get in touch with\nus first through the issue tracker with your idea so that we can help out and\npossibly guide you. Coordinating up front makes it much easier to avoid\nfrustration later on.\n\n### Code reviews\nAll submissions, including submissions by project members, require review. We\nuse Github pull requests for this purpose.\n\n### The small print\nContributions made by corporations are covered by a different agreement than\nthe one above, the\n[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2014 The Android Open Source Project\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Android Architecture Samples\n\nThese samples showcase different architectural approaches to developing Android apps. In its different branches you'll find the same app (a TODO app) implemented with small differences.\n\nIn this branch you'll find:\n*   User Interface built with **[Jetpack Compose](https://developer.android.com/jetpack/compose)** \n*   A single-activity architecture, using **[Navigation Compose](https://developer.android.com/jetpack/compose/navigation)**.\n*   A presentation layer that contains a Compose screen (View) and a **ViewModel** per screen (or feature).\n*   Reactive UIs using **[Flow](https://developer.android.com/kotlin/flow)** and **[coroutines](https://kotlinlang.org/docs/coroutines-overview.html)** for asynchronous operations.\n*   A **data layer** with a repository and two data sources (local using Room and a fake remote).\n*   Two **product flavors**, `mock` and `prod`, [to ease development and testing](https://android-developers.googleblog.com/2015/12/leveraging-product-flavors-in-android.html).\n*   A collection of unit, integration and e2e **tests**, including \"shared\" tests that can be run on emulator/device.\n*   Dependency injection using [Hilt](https://developer.android.com/training/dependency-injection/hilt-android).\n\n## Screenshots\n\n<img src=\"screenshots/screenshots.png\" alt=\"Screenshot\">\n\n## Why a to-do app?\n\nThe app in this project aims to be simple enough that you can understand it quickly, but complex enough to showcase difficult design decisions and testing scenarios. For more information, see the [app's specification](https://github.com/googlesamples/android-architecture/wiki/To-do-app-specification).\n\n## What is it not?\n*   A template. Check out the [Architecture Templates](https://github.com/android/architecture-templates) instead.\n*   A UI/Material Design sample. The interface of the app is deliberately kept simple to focus on architecture. Check out the [Compose Samples](https://github.com/android/compose-samples) instead.\n*   A real production app with network access, user authentication, etc. Check out the [Now in Android app](https://github.com/android/nowinandroid) instead.\n\n## Who is it for?\n\n*   Intermediate developers and beginners looking for a way to structure their app in a testable and maintainable way.\n*   Advanced developers looking for quick reference.\n\n## Opening a sample in Android Studio\n\nTo open one of the samples in Android Studio, begin by checking out one of the sample branches, and then open the root directory in Android Studio. The following series of steps illustrate how to open the sample.\n\nClone the repository:\n\n```\ngit clone git@github.com:android/architecture-samples.git\n```\n\nFinally open the `architecture-samples/` directory in Android Studio.\n\n### License\n\n\n```\nCopyright 2024 Google, Inc.\n\nLicensed to the Apache Software Foundation (ASF) under one or more contributor\nlicense agreements. See the NOTICE file distributed with this work for\nadditional information regarding copyright ownership. The ASF licenses this\nfile to you under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n```\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.kotlin.android)\n    alias(libs.plugins.ksp)\n    alias(libs.plugins.hilt)\n    alias(libs.plugins.compose.compiler)\n}\n\nandroid {\n    namespace = \"com.example.android.architecture.blueprints.todoapp\"\n    compileSdk = libs.versions.compileSdk.get().toInt()\n\n    defaultConfig {\n        applicationId = \"com.example.android.architecture.blueprints.main\"\n        minSdk = libs.versions.minSdk.get().toInt()\n        targetSdk = libs.versions.targetSdk.get().toInt()\n        versionCode = 1\n        versionName = \"1.0\"\n\n        testInstrumentationRunner = \"com.example.android.architecture.blueprints.todoapp.CustomTestRunner\"\n\n        javaCompileOptions {\n            annotationProcessorOptions {\n                arguments += \"room.incremental\" to \"true\"\n            }\n        }\n    }\n\n    buildTypes {\n        getByName(\"debug\") {\n            isMinifyEnabled = false\n            isTestCoverageEnabled = true\n            proguardFiles(getDefaultProguardFile(\"proguard-android.txt\"), \"proguard-rules.pro\")\n            testProguardFiles(getDefaultProguardFile(\"proguard-android.txt\"), \"proguardTest-rules.pro\")\n        }\n\n        getByName(\"release\") {\n            isMinifyEnabled = true\n            isShrinkResources = true\n            proguardFiles(getDefaultProguardFile(\"proguard-android.txt\"), \"proguard-rules.pro\")\n            testProguardFiles(getDefaultProguardFile(\"proguard-android.txt\"), \"proguardTest-rules.pro\")\n        }\n    }\n\n    // Always show the result of every unit test, even if it passes.\n    testOptions.unitTests {\n        isIncludeAndroidResources = true\n\n        all { test ->\n            with(test) {\n                testLogging {\n                    events = setOf(\n                        org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,\n                        org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED,\n                        org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,\n                        org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_OUT,\n                        org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,\n                    )\n                }\n            }\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    kotlinOptions {\n        jvmTarget = \"17\"\n    }\n\n    packaging {\n        excludes += \"META-INF/AL2.0\"\n        excludes += \"META-INF/LGPL2.1\"\n    }\n\n    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {\n        kotlinOptions {\n            freeCompilerArgs += \"-opt-in=kotlin.RequiresOptIn\"\n            freeCompilerArgs += \"-opt-in=kotlin.Experimental\"\n        }\n    }\n}\n\n/*\n Dependency versions are defined in the top level build.gradle file. This helps keeping track of\n all versions in a single place. This improves readability and helps managing project complexity.\n */\ndependencies {\n\n    // App dependencies\n    implementation(libs.androidx.annotation)\n    implementation(libs.kotlinx.coroutines.android)\n    implementation(libs.timber)\n    implementation(libs.androidx.test.espresso.idling.resources)\n\n    // Architecture Components\n    implementation(libs.room.runtime)\n    implementation(libs.room.ktx)\n    ksp(libs.room.compiler)\n    implementation(libs.androidx.lifecycle.runtimeCompose)\n    implementation(libs.androidx.lifecycle.viewModelCompose)\n\n    // Hilt\n    implementation(libs.hilt.android.core)\n    implementation(libs.androidx.hilt.navigation.compose)\n    ksp(libs.hilt.compiler)\n\n    // Jetpack Compose\n    val composeBom = platform(libs.androidx.compose.bom)\n\n    implementation(libs.androidx.activity.compose)\n    implementation(composeBom)\n    implementation(libs.androidx.compose.foundation.core)\n    implementation(libs.androidx.compose.foundation.layout)\n    implementation(libs.androidx.compose.animation)\n    implementation(libs.androidx.compose.material3)\n    implementation(libs.androidx.compose.material.iconsExtended)\n    implementation(libs.androidx.compose.ui.tooling.preview)\n    implementation(libs.androidx.navigation.compose)\n    implementation(libs.androidx.lifecycle.runtimeCompose)\n    implementation(libs.androidx.lifecycle.viewModelCompose)\n    implementation(libs.accompanist.appcompat.theme)\n    implementation(libs.accompanist.swiperefresh)\n\n    debugImplementation(composeBom)\n    debugImplementation(libs.androidx.compose.ui.tooling.core)\n    debugImplementation(libs.androidx.compose.ui.test.manifest)\n\n    // Dependencies for local unit tests\n    testImplementation(composeBom)\n    testImplementation(libs.junit4)\n    testImplementation(libs.androidx.archcore.testing)\n    testImplementation(libs.kotlinx.coroutines.android)\n    testImplementation(libs.kotlinx.coroutines.test)\n    testImplementation(libs.androidx.navigation.testing)\n    testImplementation(libs.androidx.test.espresso.core)\n    testImplementation(libs.androidx.test.espresso.contrib)\n    testImplementation(libs.androidx.test.espresso.intents)\n    testImplementation(libs.google.truth)\n    testImplementation(libs.androidx.compose.ui.test.junit)\n\n    // JVM tests - Hilt\n    testImplementation(libs.hilt.android.testing)\n    kspTest(libs.hilt.compiler)\n\n    // Dependencies for Android unit tests\n    androidTestImplementation(composeBom)\n    androidTestImplementation(libs.junit4)\n    androidTestImplementation(libs.kotlinx.coroutines.test)\n    androidTestImplementation(libs.androidx.compose.ui.test.junit)\n\n    // AndroidX Test - JVM testing\n    testImplementation(libs.androidx.test.core.ktx)\n    testImplementation(libs.androidx.test.ext)\n    testImplementation(libs.androidx.test.rules)\n    testImplementation(project(\":shared-test\"))\n\n    // AndroidX Test - Instrumented testing\n    androidTestImplementation(libs.androidx.test.core.ktx)\n    androidTestImplementation(libs.androidx.test.ext)\n    androidTestImplementation(libs.androidx.test.rules)\n    androidTestImplementation(libs.room.testing)\n    androidTestImplementation(libs.androidx.archcore.testing)\n    androidTestImplementation(libs.androidx.navigation.testing)\n    androidTestImplementation(libs.androidx.test.espresso.core)\n    androidTestImplementation(libs.androidx.test.espresso.contrib)\n    androidTestImplementation(libs.androidx.test.espresso.intents)\n    androidTestImplementation(libs.androidx.test.espresso.idling.resources)\n    androidTestImplementation(libs.androidx.test.espresso.idling.concurrent)\n    androidTestImplementation(project(\":shared-test\"))\n\n    // AndroidX Test - Hilt testing\n    androidTestImplementation(libs.hilt.android.testing)\n    kspAndroidTest(libs.hilt.compiler)\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "-dontoptimize\n\n# Some methods are only called from tests, so make sure the shrinker keeps them.\n-keep class com.example.android.architecture.blueprints.** { *; }\n\n-keep class androidx.drawerlayout.widget.DrawerLayout { *; }\n-keep class androidx.test.espresso.**\n# keep the class and specified members from being removed or renamed\n-keep class androidx.test.espresso.IdlingRegistry { *; }\n-keep class androidx.test.espresso.IdlingResource { *; }\n\n-keep class com.google.common.base.Preconditions { *; }\n\n-keep class androidx.room.RoomDataBase { *; }\n-keep class androidx.room.Room { *; }\n-keep class android.arch.** { *; }\n\n# Proguard rules that are applied to your test apk/code.\n-ignorewarnings\n\n-keepattributes *Annotation*\n\n-dontnote junit.framework.**\n-dontnote junit.runner.**\n\n-dontwarn androidx.test.**\n-dontwarn org.junit.**\n-dontwarn org.hamcrest.**\n-dontwarn com.squareup.javawriter.JavaWriter\n# Uncomment this if you use Mockito\n-dontwarn org.mockito.**\n"
  },
  {
    "path": "app/proguardTest-rules.pro",
    "content": "# Proguard rules that are applied to your test apk/code.\n-ignorewarnings\n-dontoptimize\n\n-keepattributes *Annotation*\n\n-keep class androidx.test.espresso.**\n# keep the class and specified members from being removed or renamed\n-keep class androidx.test.espresso.IdlingRegistry { *; }\n-keep class androidx.test.espresso.IdlingResource { *; }\n\n-dontnote junit.framework.**\n-dontnote junit.runner.**\n\n-dontwarn androidx.test.**\n-dontwarn org.junit.**\n-dontwarn org.hamcrest.**\n-dontwarn com.squareup.javawriter.JavaWriter\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.addedittask\n\nimport androidx.compose.material3.Surface\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.hasSetTextAction\nimport androidx.compose.ui.test.hasText\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport androidx.compose.ui.test.performTextClearance\nimport androidx.compose.ui.test.performTextInput\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.filters.MediumTest\nimport com.example.android.architecture.blueprints.todoapp.HiltTestActivity\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Assert.assertEquals\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport javax.inject.Inject\n\n/**\n * Integration test for the Add Task screen.\n */\n@RunWith(AndroidJUnit4::class)\n@MediumTest\n@HiltAndroidTest\n@ExperimentalCoroutinesApi\nclass AddEditTaskScreenTest {\n\n    @get:Rule(order = 0)\n    var hiltRule = HiltAndroidRule(this)\n\n    @get:Rule(order = 1)\n    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()\n    private val activity get() = composeTestRule.activity\n\n    @Inject\n    lateinit var repository: TaskRepository\n\n    @Before\n    fun setup() {\n        hiltRule.inject()\n\n        // GIVEN - On the \"Add Task\" screen.\n        composeTestRule.setContent {\n            TodoTheme {\n                Surface {\n                    AddEditTaskScreen(\n                        viewModel = AddEditTaskViewModel(repository, SavedStateHandle()),\n                        topBarTitle = R.string.add_task,\n                        onTaskUpdate = { },\n                        onBack = { },\n                    )\n                }\n            }\n        }\n    }\n\n    @Test\n    fun emptyTask_isNotSaved() {\n        // WHEN - Enter invalid title and description combination and click save\n        findTextField(R.string.title_hint).performTextClearance()\n        findTextField(R.string.description_hint).performTextClearance()\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))\n            .performClick()\n\n        // THEN - Entered Task is still displayed (a correct task would close it).\n        composeTestRule\n            .onNodeWithText(activity.getString(R.string.empty_task_message))\n            .assertIsDisplayed()\n    }\n\n    @Test\n    fun validTask_isSaved() = runTest {\n        // WHEN - Valid title and description combination and click save\n        findTextField(R.string.title_hint).performTextInput(\"title\")\n        findTextField(R.string.description_hint).performTextInput(\"description\")\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))\n            .performClick()\n\n        // THEN - Verify that the repository saved the task\n        val tasks = repository.getTasks(true)\n        assertEquals(1, tasks.size)\n        assertEquals(\"title\", tasks[0].title)\n        assertEquals(\"description\", tasks[0].description)\n    }\n\n    private fun findTextField(text: Int): SemanticsNodeInteraction {\n        return composeTestRule.onNode(\n            hasSetTextAction() and hasText(activity.getString(text))\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDaoTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.local\n\nimport androidx.room.Room\nimport androidx.test.core.app.ApplicationProvider.getApplicationContext\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.filters.SmallTest\nimport junit.framework.TestCase.assertEquals\nimport junit.framework.TestCase.assertNotNull\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n@ExperimentalCoroutinesApi\n@RunWith(AndroidJUnit4::class)\n@SmallTest\nclass TaskDaoTest {\n\n    // using an in-memory database because the information stored here disappears when the\n    // process is killed\n    private lateinit var database: ToDoDatabase\n\n    // Ensure that we use a new database for each test.\n    @Before\n    fun initDb() {\n        database = Room.inMemoryDatabaseBuilder(\n            getApplicationContext(),\n            ToDoDatabase::class.java\n        ).allowMainThreadQueries().build()\n    }\n    @Test\n    fun insertTaskAndGetById() = runTest {\n        // GIVEN - insert a task\n        val task = LocalTask(\n            title = \"title\",\n            description = \"description\",\n            id = \"id\",\n            isCompleted = false,\n        )\n        database.taskDao().upsert(task)\n\n        // WHEN - Get the task by id from the database\n        val loaded = database.taskDao().getById(task.id)\n\n        // THEN - The loaded data contains the expected values\n        assertNotNull(loaded as LocalTask)\n        assertEquals(task.id, loaded.id)\n        assertEquals(task.title, loaded.title)\n        assertEquals(task.description, loaded.description)\n        assertEquals(task.isCompleted, loaded.isCompleted)\n    }\n\n    @Test\n    fun insertTaskReplacesOnConflict() = runTest {\n        // Given that a task is inserted\n        val task = LocalTask(\n            title = \"title\",\n            description = \"description\",\n            id = \"id\",\n            isCompleted = false,\n        )\n        database.taskDao().upsert(task)\n\n        // When a task with the same id is inserted\n        val newTask = LocalTask(\n            title = \"title2\",\n            description = \"description2\",\n            isCompleted = true,\n            id = task.id\n        )\n        database.taskDao().upsert(newTask)\n\n        // THEN - The loaded data contains the expected values\n        val loaded = database.taskDao().getById(task.id)\n        assertEquals(task.id, loaded?.id)\n        assertEquals(\"title2\", loaded?.title)\n        assertEquals(\"description2\", loaded?.description)\n        assertEquals(true, loaded?.isCompleted)\n    }\n\n    @Test\n    fun insertTaskAndGetTasks() = runTest {\n        // GIVEN - insert a task\n        val task = LocalTask(\n            title = \"title\",\n            description = \"description\",\n            id = \"id\",\n            isCompleted = false,\n        )\n        database.taskDao().upsert(task)\n\n        // WHEN - Get tasks from the database\n        val tasks = database.taskDao().getAll()\n\n        // THEN - There is only 1 task in the database, and contains the expected values\n        assertEquals(1, tasks.size)\n        assertEquals(tasks[0].id, task.id)\n        assertEquals(tasks[0].title, task.title)\n        assertEquals(tasks[0].description, task.description)\n        assertEquals(tasks[0].isCompleted, task.isCompleted)\n    }\n\n    @Test\n    fun updateTaskAndGetById() = runTest {\n        // When inserting a task\n        val originalTask = LocalTask(\n            title = \"title\",\n            description = \"description\",\n            id = \"id\",\n            isCompleted = false,\n        )\n\n        database.taskDao().upsert(originalTask)\n\n        // When the task is updated\n        val updatedTask = LocalTask(\n            title = \"new title\",\n            description = \"new description\",\n            isCompleted = true,\n            id = originalTask.id\n        )\n        database.taskDao().upsert(updatedTask)\n\n        // THEN - The loaded data contains the expected values\n        val loaded = database.taskDao().getById(originalTask.id)\n        assertEquals(originalTask.id, loaded?.id)\n        assertEquals(\"new title\", loaded?.title)\n        assertEquals(\"new description\", loaded?.description)\n        assertEquals(true, loaded?.isCompleted)\n    }\n\n    @Test\n    fun updateCompletedAndGetById() = runTest {\n        // When inserting a task\n        val task = LocalTask(\n            title = \"title\",\n            description = \"description\",\n            id = \"id\",\n            isCompleted = true\n        )\n        database.taskDao().upsert(task)\n\n        // When the task is updated\n        database.taskDao().updateCompleted(task.id, false)\n\n        // THEN - The loaded data contains the expected values\n        val loaded = database.taskDao().getById(task.id)\n        assertEquals(task.id, loaded?.id)\n        assertEquals(task.title, loaded?.title)\n        assertEquals(task.description, loaded?.description)\n        assertEquals(false, loaded?.isCompleted)\n    }\n\n    @Test\n    fun deleteTaskByIdAndGettingTasks() = runTest {\n        // Given a task inserted\n        val task = LocalTask(\n            title = \"title\",\n            description = \"description\",\n            id = \"id\",\n            isCompleted = false,\n        )\n        database.taskDao().upsert(task)\n\n        // When deleting a task by id\n        database.taskDao().deleteById(task.id)\n\n        // THEN - The list is empty\n        val tasks = database.taskDao().getAll()\n        assertEquals(true, tasks.isEmpty())\n    }\n\n    @Test\n    fun deleteTasksAndGettingTasks() = runTest {\n        // Given a task inserted\n        database.taskDao().upsert(\n            LocalTask(\n                title = \"title\",\n                description = \"description\",\n                id = \"id\",\n                isCompleted = false,\n            )\n        )\n\n        // When deleting all tasks\n        database.taskDao().deleteAll()\n\n        // THEN - The list is empty\n        val tasks = database.taskDao().getAll()\n        assertEquals(true, tasks.isEmpty())\n    }\n\n    @Test\n    fun deleteCompletedTasksAndGettingTasks() = runTest {\n        // Given a completed task inserted\n        database.taskDao().upsert(\n            LocalTask(title = \"completed\", description = \"task\", id = \"id\", isCompleted = true)\n        )\n\n        // When deleting completed tasks\n        database.taskDao().deleteCompleted()\n\n        // THEN - The list is empty\n        val tasks = database.taskDao().getAll()\n        assertEquals(true, tasks.isEmpty())\n    }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreenTest.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.statistics\n\nimport androidx.compose.material3.Surface\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.filters.MediumTest\nimport com.example.android.architecture.blueprints.todoapp.HiltTestActivity\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport javax.inject.Inject\n\n/**\n * Integration test for the statistics screen.\n */\n@RunWith(AndroidJUnit4::class)\n@MediumTest\n@HiltAndroidTest\n@ExperimentalCoroutinesApi\nclass StatisticsScreenTest {\n\n    @get:Rule(order = 0)\n    var hiltRule = HiltAndroidRule(this)\n\n    @get:Rule(order = 1)\n    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()\n    private val activity get() = composeTestRule.activity\n\n    @Inject\n    lateinit var repository: TaskRepository\n\n    @Before\n    fun setup() {\n        hiltRule.inject()\n    }\n\n    @Test\n    fun tasks_showsNonEmptyMessage() = runTest {\n        // Given some tasks\n        repository.apply {\n            createTask(\"Title1\", \"Description1\")\n            createTask(\"Title2\", \"Description2\").also {\n                completeTask(it)\n            }\n        }\n\n        composeTestRule.setContent {\n            TodoTheme {\n                Surface {\n                    StatisticsScreen(\n                        openDrawer = { },\n                        viewModel = StatisticsViewModel(repository)\n                    )\n                }\n            }\n        }\n\n        val expectedActiveTaskText = activity.getString(R.string.statistics_active_tasks, 50.0f)\n        val expectedCompletedTaskText = activity\n            .getString(R.string.statistics_completed_tasks, 50.0f)\n\n        // check that both info boxes are displayed and contain the correct info\n        composeTestRule.onNodeWithText(expectedActiveTaskText).assertIsDisplayed()\n        composeTestRule.onNodeWithText(expectedCompletedTaskText).assertIsDisplayed()\n    }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreenTest.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.taskdetail\n\nimport androidx.compose.material3.Surface\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsOff\nimport androidx.compose.ui.test.assertIsOn\nimport androidx.compose.ui.test.isToggleable\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.filters.MediumTest\nimport com.example.android.architecture.blueprints.todoapp.HiltTestActivity\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport javax.inject.Inject\n\n/**\n * Integration test for the Task Details screen.\n */\n@MediumTest\n@RunWith(AndroidJUnit4::class)\n@HiltAndroidTest\n@ExperimentalCoroutinesApi\nclass TaskDetailScreenTest {\n\n    @get:Rule(order = 0)\n    var hiltRule = HiltAndroidRule(this)\n\n    @get:Rule(order = 1)\n    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()\n\n    @Inject\n    lateinit var repository: TaskRepository\n\n    @Before\n    fun setup() {\n        hiltRule.inject()\n    }\n\n    @Test\n    fun activeTaskDetails_DisplayedInUi() = runTest {\n        // GIVEN - Add active (incomplete) task to the DB\n        val activeTaskId = repository.createTask(\n            title = \"Active Task\",\n            description = \"AndroidX Rocks\"\n        )\n\n        // WHEN - Details screen is opened\n        setContent(activeTaskId)\n\n        // THEN - Task details are displayed on the screen\n        // make sure that the title/description are both shown and correct\n        composeTestRule.onNodeWithText(\"Active Task\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"AndroidX Rocks\").assertIsDisplayed()\n        // and make sure the \"active\" checkbox is shown unchecked\n        composeTestRule.onNode(isToggleable()).assertIsOff()\n    }\n\n    @Test\n    fun completedTaskDetails_DisplayedInUi() = runTest {\n        // GIVEN - Add completed task to the DB\n        val completedTaskId = repository.createTask(\"Completed Task\", \"AndroidX Rocks\")\n        repository.completeTask(completedTaskId)\n\n        // WHEN - Details screen is opened\n        setContent(completedTaskId)\n\n        // THEN - Task details are displayed on the screen\n        // make sure that the title/description are both shown and correct\n        composeTestRule.onNodeWithText(\"Completed Task\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"AndroidX Rocks\").assertIsDisplayed()\n        // and make sure the \"active\" checkbox is shown unchecked\n        composeTestRule.onNode(isToggleable()).assertIsOn()\n    }\n\n    private fun setContent(activeTaskId: String) {\n        composeTestRule.setContent {\n            TodoTheme {\n                Surface {\n                    TaskDetailScreen(\n                        viewModel = TaskDetailViewModel(\n                            repository,\n                            SavedStateHandle(mapOf(\"taskId\" to activeTaskId))\n                        ),\n                        onEditTask = { /*TODO*/ },\n                        onBack = { },\n                        onDeleteTask = { },\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.tasks\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsNotDisplayed\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onAllNodesWithText\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport androidx.test.espresso.Espresso.pressBack\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.filters.LargeTest\nimport com.example.android.architecture.blueprints.todoapp.HiltTestActivity\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoNavGraph\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Assert.assertTrue\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport javax.inject.Inject\n\n/**\n * Tests for scenarios that requires navigating within the app.\n */\n@RunWith(AndroidJUnit4::class)\n@LargeTest\n@HiltAndroidTest\nclass AppNavigationTest {\n\n    @get:Rule(order = 0)\n    var hiltRule = HiltAndroidRule(this)\n\n    // Executes tasks in the Architecture Components in the same thread\n    @get:Rule(order = 1)\n    var instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @get:Rule(order = 2)\n    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()\n    private val activity get() = composeTestRule.activity\n\n    @Inject\n    lateinit var taskRepository: TaskRepository\n\n    @Before\n    fun init() {\n        hiltRule.inject()\n    }\n\n    @Test\n    fun drawerNavigationFromTasksToStatistics() {\n        setContent()\n\n        openDrawer()\n        // Start statistics screen.\n        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title)).performClick()\n        // Check that statistics screen was opened.\n        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_no_tasks))\n            .assertIsDisplayed()\n\n        openDrawer()\n        // Start tasks screen.\n        composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).performClick()\n        // Check that tasks screen was opened.\n        composeTestRule.onNodeWithText(activity.getString(R.string.no_tasks_all))\n            .assertIsDisplayed()\n    }\n\n    @Test\n    fun tasksScreen_clickOnAndroidHomeIcon_OpensNavigation() {\n        setContent()\n\n        // Check that left drawer is closed at startup\n        composeTestRule.onNodeWithText(activity.getString(R.string.list_title))\n            .assertIsNotDisplayed()\n        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title))\n            .assertIsNotDisplayed()\n\n        openDrawer()\n\n        // Check if drawer is open\n        composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title))\n            .assertIsDisplayed()\n    }\n\n    @Test\n    fun statsScreen_clickOnAndroidHomeIcon_OpensNavigation() {\n        setContent()\n\n        // When the user navigates to the stats screen\n        openDrawer()\n        composeTestRule.onNodeWithText(activity.getString(R.string.statistics_title)).performClick()\n\n        composeTestRule.onNodeWithText(activity.getString(R.string.list_title))\n            .assertIsNotDisplayed()\n\n        openDrawer()\n\n        // Check if drawer is open\n        composeTestRule.onNodeWithText(activity.getString(R.string.list_title)).assertIsDisplayed()\n        assertTrue(\n            composeTestRule.onAllNodesWithText(activity.getString(R.string.statistics_title))\n                .fetchSemanticsNodes().isNotEmpty()\n        )\n    }\n\n    @Test\n    fun taskDetailScreen_doubleUIBackButton() = runTest {\n        val taskName = \"UI <- button\"\n        taskRepository.createTask(taskName, \"Description\")\n\n        setContent()\n\n        // Click on the task on the list\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskName).performClick()\n\n        // Click on the edit task button\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))\n            .assertIsDisplayed()\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))\n            .performClick()\n\n        // Confirm that if we click \"<-\" once, we end up back at the task details page\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))\n            .performClick()\n        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()\n\n        // Confirm that if we click \"<-\" a second time, we end up back at the home screen\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))\n            .performClick()\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n    }\n\n    @Test\n    fun taskDetailScreen_doubleBackButton() = runTest {\n        val taskName = \"Back button\"\n        taskRepository.createTask(taskName, \"Description\")\n\n        setContent()\n\n        // Click on the task on the list\n        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskName).performClick()\n        // Click on the edit task button\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))\n            .performClick()\n\n        // Confirm that if we click back once, we end up back at the task details page\n        pressBack()\n        composeTestRule.onNodeWithText(taskName).assertIsDisplayed()\n\n        // Confirm that if we click back a second time, we end up back at the home screen\n        pressBack()\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n    }\n\n    private fun setContent() {\n        composeTestRule.setContent {\n            TodoTheme {\n                TodoNavGraph()\n            }\n        }\n    }\n\n    private fun openDrawer() {\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.open_drawer))\n            .performClick()\n    }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.tasks\n\nimport androidx.annotation.StringRes\nimport androidx.compose.material3.Surface\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.isToggleable\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.filters.MediumTest\nimport com.example.android.architecture.blueprints.todoapp.HiltTestActivity\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport javax.inject.Inject\n\n/**\n * Integration test for the Task List screen.\n */\n// TODO - Move to the sharedTest folder when https://issuetracker.google.com/224974381 is fixed\n@RunWith(AndroidJUnit4::class)\n@MediumTest\n// @LooperMode(LooperMode.Mode.PAUSED)\n// @TextLayoutMode(TextLayoutMode.Mode.REALISTIC)\n@HiltAndroidTest\n@OptIn(ExperimentalCoroutinesApi::class)\nclass TasksScreenTest {\n\n    @get:Rule(order = 0)\n    var hiltRule = HiltAndroidRule(this)\n\n    @get:Rule(order = 1)\n    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()\n    private val activity get() = composeTestRule.activity\n\n    @Inject\n    lateinit var repository: TaskRepository\n\n    @Before\n    fun init() {\n        hiltRule.inject()\n    }\n\n    @Test\n    fun displayTask_whenRepositoryHasData() = runTest {\n        // GIVEN - One task already in the repository\n        repository.createTask(\"TITLE1\", \"DESCRIPTION1\")\n\n        // WHEN - On startup\n        setContent()\n\n        // THEN - Verify task is displayed on screen\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n    }\n\n    @Test\n    fun displayActiveTask() = runTest {\n        repository.createTask(\"TITLE1\", \"DESCRIPTION1\")\n\n        setContent()\n\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n\n        openFilterAndSelectOption(R.string.nav_active)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n\n        openFilterAndSelectOption(R.string.nav_completed)\n\n        composeTestRule.onNodeWithText(\"TITLE1\").assertDoesNotExist()\n    }\n\n    @Test\n    fun displayCompletedTask() = runTest {\n        repository.apply {\n            createTask(\"TITLE1\", \"DESCRIPTION1\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n\n        openFilterAndSelectOption(R.string.nav_active)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertDoesNotExist()\n\n        openFilterAndSelectOption(R.string.nav_completed)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n    }\n\n    @Test\n    fun markTaskAsComplete() = runTest {\n        repository.createTask(\"TITLE1\", \"DESCRIPTION1\")\n\n        setContent()\n\n        // Mark the task as complete\n        composeTestRule.onNode(isToggleable()).performClick()\n\n        // Verify task is shown as complete\n        openFilterAndSelectOption(R.string.nav_all)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n        openFilterAndSelectOption(R.string.nav_active)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertDoesNotExist()\n        openFilterAndSelectOption(R.string.nav_completed)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n    }\n\n    @Test\n    fun markTaskAsActive() = runTest {\n        repository.apply {\n            createTask(\"TITLE1\", \"DESCRIPTION1\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        // Mark the task as active\n        composeTestRule.onNode(isToggleable()).performClick()\n\n        // Verify task is shown as active\n        openFilterAndSelectOption(R.string.nav_all)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n        openFilterAndSelectOption(R.string.nav_active)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n        openFilterAndSelectOption(R.string.nav_completed)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertDoesNotExist()\n    }\n\n    @Test\n    fun showAllTasks() = runTest {\n        // Add one active task and one completed task\n        repository.apply {\n            createTask(\"TITLE1\", \"DESCRIPTION1\")\n            createTask(\"TITLE2\", \"DESCRIPTION2\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        // Verify that both of our tasks are shown\n        openFilterAndSelectOption(R.string.nav_all)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE2\").assertIsDisplayed()\n    }\n\n    @Test\n    fun showActiveTasks() = runTest {\n        // Add 2 active tasks and one completed task\n        repository.apply {\n            createTask(\"TITLE1\", \"DESCRIPTION1\")\n            createTask(\"TITLE2\", \"DESCRIPTION2\")\n            createTask(\"TITLE3\", \"DESCRIPTION3\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        // Verify that the active tasks (but not the completed task) are shown\n        openFilterAndSelectOption(R.string.nav_active)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE2\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE3\").assertDoesNotExist()\n    }\n\n    @Test\n    fun showCompletedTasks() = runTest {\n        // Add one active task and 2 completed tasks\n        repository.apply {\n            createTask(\"TITLE1\", \"DESCRIPTION1\")\n            createTask(\"TITLE2\", \"DESCRIPTION2\").also { completeTask(it) }\n            createTask(\"TITLE3\", \"DESCRIPTION3\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        // Verify that the completed tasks (but not the active task) are shown\n        openFilterAndSelectOption(R.string.nav_completed)\n        composeTestRule.onNodeWithText(\"TITLE1\").assertDoesNotExist()\n        composeTestRule.onNodeWithText(\"TITLE2\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE3\").assertIsDisplayed()\n    }\n\n    @Test\n    fun clearCompletedTasks() = runTest {\n        // Add one active task and one completed task\n        repository.apply {\n            createTask(\"TITLE1\", \"DESCRIPTION1\")\n            createTask(\"TITLE2\", \"DESCRIPTION2\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        // Click clear completed in menu\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_more))\n            .performClick()\n        composeTestRule.onNodeWithText(activity.getString(R.string.menu_clear)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(activity.getString(R.string.menu_clear)).performClick()\n\n        openFilterAndSelectOption(R.string.nav_all)\n        // Verify that only the active task is shown\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE2\").assertDoesNotExist()\n    }\n\n    @Test\n    fun noTasks_AllTasksFilter_AddTaskViewVisible() {\n        setContent()\n\n        openFilterAndSelectOption(R.string.nav_all)\n\n        // Verify the \"You have no tasks!\" text is shown\n        composeTestRule.onNodeWithText(\"You have no tasks!\").assertIsDisplayed()\n    }\n\n    @Test\n    fun noTasks_CompletedTasksFilter_AddTaskViewNotVisible() {\n        setContent()\n\n        openFilterAndSelectOption(R.string.nav_completed)\n        // Verify the \"You have no completed tasks!\" text is shown\n        composeTestRule.onNodeWithText(\"You have no completed tasks!\").assertIsDisplayed()\n    }\n\n    @Test\n    fun noTasks_ActiveTasksFilter_AddTaskViewNotVisible() {\n        setContent()\n\n        openFilterAndSelectOption(R.string.nav_active)\n        // Verify the \"You have no active tasks!\" text is shown\n        composeTestRule.onNodeWithText(\"You have no active tasks!\").assertIsDisplayed()\n    }\n\n    private fun setContent() {\n        composeTestRule.setContent {\n            TodoTheme {\n                Surface {\n                    TasksScreen(\n                        viewModel = TasksViewModel(repository, SavedStateHandle()),\n                        userMessage = R.string.successfully_added_task_message,\n                        onUserMessageDisplayed = { },\n                        onAddTask = { },\n                        onTaskClick = { },\n                        openDrawer = { }\n                    )\n                }\n            }\n        }\n    }\n\n    private fun openFilterAndSelectOption(@StringRes option: Int) {\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))\n            .performClick()\n        composeTestRule.onNodeWithText(activity.getString(option)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(activity.getString(option)).performClick()\n    }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksTest.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.tasks\n\nimport androidx.arch.core.executor.testing.InstantTaskExecutorRule\nimport androidx.compose.ui.test.SemanticsNodeInteraction\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.assertIsOff\nimport androidx.compose.ui.test.assertIsOn\nimport androidx.compose.ui.test.hasSetTextAction\nimport androidx.compose.ui.test.hasText\nimport androidx.compose.ui.test.isToggleable\nimport androidx.compose.ui.test.junit4.createAndroidComposeRule\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport androidx.compose.ui.test.performTextInput\nimport androidx.compose.ui.test.performTextReplacement\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.filters.LargeTest\nimport com.example.android.architecture.blueprints.todoapp.HiltTestActivity\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoNavGraph\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.hilt.android.testing.HiltAndroidRule\nimport dagger.hilt.android.testing.HiltAndroidTest\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport org.junit.runner.RunWith\nimport javax.inject.Inject\n\n/**\n * Large End-to-End test for the tasks module.\n */\n@RunWith(AndroidJUnit4::class)\n@LargeTest\n@HiltAndroidTest\n@OptIn(ExperimentalCoroutinesApi::class)\nclass TasksTest {\n\n    @get:Rule(order = 0)\n    var hiltRule = HiltAndroidRule(this)\n\n    // Executes tasks in the Architecture Components in the same thread\n    @get:Rule(order = 1)\n    var instantTaskExecutorRule = InstantTaskExecutorRule()\n\n    @get:Rule(order = 2)\n    val composeTestRule = createAndroidComposeRule<HiltTestActivity>()\n    private val activity get() = composeTestRule.activity\n\n    @Inject\n    lateinit var repository: TaskRepository\n\n    @Before\n    fun init() {\n        hiltRule.inject()\n    }\n\n    @Test\n    fun editTask() = runTest {\n        val originalTaskTitle = \"TITLE1\"\n        repository.createTask(originalTaskTitle, \"DESCRIPTION\")\n\n        setContent()\n\n        // Click on the task on the list and verify that all the data is correct\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(originalTaskTitle).assertIsDisplayed()\n        composeTestRule.onNodeWithText(originalTaskTitle).performClick()\n\n        // Task detail screen\n        composeTestRule.onNodeWithText(activity.getString(R.string.task_details))\n            .assertIsDisplayed()\n        composeTestRule.onNodeWithText(originalTaskTitle).assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"DESCRIPTION\").assertIsDisplayed()\n        composeTestRule.onNode(isToggleable()).assertIsOff()\n\n        // Click on the edit button, edit, and save\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.edit_task))\n            .performClick()\n        composeTestRule.onNodeWithText(activity.getString(R.string.edit_task)).assertIsDisplayed()\n        findTextField(originalTaskTitle).performTextReplacement(\"NEW TITLE\")\n        findTextField(\"DESCRIPTION\").performTextReplacement(\"NEW DESCRIPTION\")\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))\n            .performClick()\n\n        // Verify task is displayed on screen in the task list.\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"NEW TITLE\").assertIsDisplayed()\n        // Verify previous task is not displayed\n        composeTestRule.onNodeWithText(originalTaskTitle).assertDoesNotExist()\n    }\n\n    @Test\n    fun createOneTask_deleteTask() {\n        setContent()\n\n        val taskTitle = \"TITLE1\"\n        // Add active task\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.add_task))\n            .performClick()\n        findTextField(R.string.title_hint).performTextInput(taskTitle)\n        findTextField(R.string.description_hint).performTextInput(\"DESCRIPTION\")\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))\n            .performClick()\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()\n\n        // Open the task detail screen\n        composeTestRule.onNodeWithText(taskTitle).performClick()\n        composeTestRule.onNodeWithText(activity.getString(R.string.task_details))\n            .assertIsDisplayed()\n        // Click delete task in menu\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_delete_task))\n            .performClick()\n\n        // Verify it was deleted\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))\n            .performClick()\n        composeTestRule.onNodeWithText(activity.getString(R.string.nav_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).assertDoesNotExist()\n    }\n\n    @Test\n    fun createTwoTasks_deleteOneTask() = runTest {\n        repository.apply {\n            createTask(\"TITLE1\", \"DESCRIPTION\")\n            createTask(\"TITLE2\", \"DESCRIPTION\")\n        }\n\n        setContent()\n\n        // Open the second task in details view\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE2\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE2\").performClick()\n        // Click delete task in menu\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_delete_task))\n            .performClick()\n\n        // Verify only one task was deleted\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_filter))\n            .performClick()\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).performClick()\n        composeTestRule.onNodeWithText(\"TITLE1\").assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"TITLE2\").assertDoesNotExist()\n    }\n\n    @Test\n    fun markTaskAsCompleteOnDetailScreen_taskIsCompleteInList() = runTest {\n        // Add 1 active task\n        val taskTitle = \"COMPLETED\"\n        repository.createTask(taskTitle, \"DESCRIPTION\")\n\n        setContent()\n\n        // Click on the task on the list\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).performClick()\n\n        // Click on the checkbox in task details screen\n        composeTestRule.onNode(isToggleable()).performClick()\n\n        // Click on the navigation up button to go back to the list\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))\n            .performClick()\n\n        // Check that the task is marked as completed\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNode(isToggleable()).assertIsOn()\n    }\n\n    @Test\n    fun markTaskAsActiveOnDetailScreen_taskIsActiveInList() = runTest {\n        // Add 1 completed task\n        val taskTitle = \"ACTIVE\"\n        repository.apply {\n            createTask(taskTitle, \"DESCRIPTION\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        // Click on the task on the list\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).performClick()\n\n        // Click on the checkbox in task details screen\n        composeTestRule.onNode(isToggleable()).performClick()\n\n        // Click on the navigation up button to go back to the list\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))\n            .performClick()\n\n        // Check that the task is marked as active\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNode(isToggleable()).assertIsOff()\n    }\n\n    @Test\n    fun markTaskAsCompleteAndActiveOnDetailScreen_taskIsActiveInList() = runTest {\n        // Add 1 active task\n        val taskTitle = \"ACT-COMP\"\n        repository.createTask(taskTitle, \"DESCRIPTION\")\n\n        setContent()\n\n        // Click on the task on the list\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).performClick()\n\n        // Click on the checkbox in task details screen\n        composeTestRule.onNode(isToggleable()).performClick()\n        // Click again to restore it to original state\n        composeTestRule.onNode(isToggleable()).performClick()\n\n        // Click on the navigation up button to go back to the list\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))\n            .performClick()\n\n        // Check that the task is marked as active\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNode(isToggleable()).assertIsOff()\n    }\n\n    @Test\n    fun markTaskAsActiveAndCompleteOnDetailScreen_taskIsCompleteInList() = runTest {\n        // Add 1 completed task\n        val taskTitle = \"COMP-ACT\"\n        repository.apply {\n            createTask(taskTitle, \"DESCRIPTION\").also { completeTask(it) }\n        }\n\n        setContent()\n\n        // Click on the task on the list\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).assertIsDisplayed()\n        composeTestRule.onNodeWithText(taskTitle).performClick()\n        // Click on the checkbox in task details screen\n        composeTestRule.onNode(isToggleable()).performClick()\n        // Click again to restore it to original state\n        composeTestRule.onNode(isToggleable()).performClick()\n\n        // Click on the navigation up button to go back to the list\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.menu_back))\n            .performClick()\n\n        // Check that the task is marked as active\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNode(isToggleable()).assertIsOn()\n    }\n\n    @Test\n    fun createTask() {\n        setContent()\n\n        // Click on the \"+\" button, add details, and save\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.add_task))\n            .performClick()\n        findTextField(R.string.title_hint).performTextInput(\"title\")\n        findTextField(R.string.description_hint).performTextInput(\"description\")\n        composeTestRule.onNodeWithContentDescription(activity.getString(R.string.cd_save_task))\n            .performClick()\n\n        // Then verify task is displayed on screen\n        composeTestRule.onNodeWithText(activity.getString(R.string.label_all)).assertIsDisplayed()\n        composeTestRule.onNodeWithText(\"title\").assertIsDisplayed()\n    }\n\n    private fun setContent() {\n        composeTestRule.setContent {\n            TodoTheme {\n                TodoNavGraph()\n            }\n        }\n    }\n\n    private fun findTextField(textId: Int): SemanticsNodeInteraction {\n        return composeTestRule.onNode(\n            hasSetTextAction() and hasText(activity.getString(textId))\n        )\n    }\n\n    private fun findTextField(text: String): SemanticsNodeInteraction {\n        return composeTestRule.onNode(\n            hasSetTextAction() and hasText(text)\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/debug/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2022 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.android.architecture.blueprints.todoapp\">\n\n    <application>\n        <activity\n            android:name=\".HiltTestActivity\"\n            android:exported=\"false\"  />\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/debug/java/com/example/android/architecture/blueprints/todoapp/HiltTestActivity.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp\n\nimport androidx.activity.ComponentActivity\nimport dagger.hilt.android.AndroidEntryPoint\n\n@AndroidEntryPoint\nclass HiltTestActivity : ComponentActivity()\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n\n    <application\n        android:allowBackup=\"false\"\n        android:name=\".TodoApplication\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity\n            android:name=\"com.example.android.architecture.blueprints.todoapp.TodoActivity\"\n            android:windowSoftInputMode=\"adjustResize\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoActivity.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport dagger.hilt.android.AndroidEntryPoint\n\n/**\n * Main activity for the todoapp\n */\n@AndroidEntryPoint\nclass TodoActivity : ComponentActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enableEdgeToEdge()\n        setContent {\n            TodoTheme {\n                TodoNavGraph()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoApplication.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp\n\nimport android.app.Application\nimport dagger.hilt.android.HiltAndroidApp\nimport timber.log.Timber\nimport timber.log.Timber.DebugTree\n\n/**\n * Application that sets up Timber in the DEBUG BuildConfig.\n * Read Timber's documentation for production setups.\n */\n@HiltAndroidApp\nclass TodoApplication : Application() {\n\n    override fun onCreate() {\n        super.onCreate()\n        if (BuildConfig.DEBUG) Timber.plant(DebugTree())\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp\n\nimport android.app.Activity\nimport androidx.compose.material3.DrawerState\nimport androidx.compose.material3.DrawerValue\nimport androidx.compose.material3.rememberDrawerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.navigation.NavHostController\nimport androidx.navigation.NavType\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport androidx.navigation.compose.rememberNavController\nimport androidx.navigation.navArgument\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TASK_ID_ARG\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TITLE_ARG\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.USER_MESSAGE_ARG\nimport com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskScreen\nimport com.example.android.architecture.blueprints.todoapp.statistics.StatisticsScreen\nimport com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailScreen\nimport com.example.android.architecture.blueprints.todoapp.tasks.TasksScreen\nimport com.example.android.architecture.blueprints.todoapp.util.AppModalDrawer\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\n\n@Composable\nfun TodoNavGraph(\n    modifier: Modifier = Modifier,\n    navController: NavHostController = rememberNavController(),\n    coroutineScope: CoroutineScope = rememberCoroutineScope(),\n    drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed),\n    startDestination: String = TodoDestinations.TASKS_ROUTE,\n    navActions: TodoNavigationActions = remember(navController) {\n        TodoNavigationActions(navController)\n    }\n) {\n    val currentNavBackStackEntry by navController.currentBackStackEntryAsState()\n    val currentRoute = currentNavBackStackEntry?.destination?.route ?: startDestination\n\n    NavHost(\n        navController = navController,\n        startDestination = startDestination,\n        modifier = modifier\n    ) {\n        composable(\n            TodoDestinations.TASKS_ROUTE,\n            arguments = listOf(\n                navArgument(USER_MESSAGE_ARG) { type = NavType.IntType; defaultValue = 0 }\n            )\n        ) { entry ->\n            AppModalDrawer(drawerState, currentRoute, navActions) {\n                TasksScreen(\n                    userMessage = entry.arguments?.getInt(USER_MESSAGE_ARG)!!,\n                    onUserMessageDisplayed = { entry.arguments?.putInt(USER_MESSAGE_ARG, 0) },\n                    onAddTask = { navActions.navigateToAddEditTask(R.string.add_task, null) },\n                    onTaskClick = { task -> navActions.navigateToTaskDetail(task.id) },\n                    openDrawer = { coroutineScope.launch { drawerState.open() } }\n                )\n            }\n        }\n        composable(TodoDestinations.STATISTICS_ROUTE) {\n            AppModalDrawer(drawerState, currentRoute, navActions) {\n                StatisticsScreen(openDrawer = { coroutineScope.launch { drawerState.open() } })\n            }\n        }\n        composable(\n            TodoDestinations.ADD_EDIT_TASK_ROUTE,\n            arguments = listOf(\n                navArgument(TITLE_ARG) { type = NavType.IntType },\n                navArgument(TASK_ID_ARG) { type = NavType.StringType; nullable = true },\n            )\n        ) { entry ->\n            val taskId = entry.arguments?.getString(TASK_ID_ARG)\n            AddEditTaskScreen(\n                topBarTitle = entry.arguments?.getInt(TITLE_ARG)!!,\n                onTaskUpdate = {\n                    navActions.navigateToTasks(\n                        if (taskId == null) ADD_EDIT_RESULT_OK else EDIT_RESULT_OK\n                    )\n                },\n                onBack = { navController.popBackStack() }\n            )\n        }\n        composable(TodoDestinations.TASK_DETAIL_ROUTE) {\n            TaskDetailScreen(\n                onEditTask = { taskId ->\n                    navActions.navigateToAddEditTask(R.string.edit_task, taskId)\n                },\n                onBack = { navController.popBackStack() },\n                onDeleteTask = { navActions.navigateToTasks(DELETE_RESULT_OK) }\n            )\n        }\n    }\n}\n\n// Keys for navigation\nconst val ADD_EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 1\nconst val DELETE_RESULT_OK = Activity.RESULT_FIRST_USER + 2\nconst val EDIT_RESULT_OK = Activity.RESULT_FIRST_USER + 3\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp\n\nimport androidx.navigation.NavGraph.Companion.findStartDestination\nimport androidx.navigation.NavHostController\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TASK_ID_ARG\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TITLE_ARG\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.USER_MESSAGE_ARG\nimport com.example.android.architecture.blueprints.todoapp.TodoScreens.ADD_EDIT_TASK_SCREEN\nimport com.example.android.architecture.blueprints.todoapp.TodoScreens.STATISTICS_SCREEN\nimport com.example.android.architecture.blueprints.todoapp.TodoScreens.TASKS_SCREEN\nimport com.example.android.architecture.blueprints.todoapp.TodoScreens.TASK_DETAIL_SCREEN\n\n/**\n * Screens used in [TodoDestinations]\n */\nprivate object TodoScreens {\n    const val TASKS_SCREEN = \"tasks\"\n    const val STATISTICS_SCREEN = \"statistics\"\n    const val TASK_DETAIL_SCREEN = \"task\"\n    const val ADD_EDIT_TASK_SCREEN = \"addEditTask\"\n}\n\n/**\n * Arguments used in [TodoDestinations] routes\n */\nobject TodoDestinationsArgs {\n    const val USER_MESSAGE_ARG = \"userMessage\"\n    const val TASK_ID_ARG = \"taskId\"\n    const val TITLE_ARG = \"title\"\n}\n\n/**\n * Destinations used in the [TodoActivity]\n */\nobject TodoDestinations {\n    const val TASKS_ROUTE = \"$TASKS_SCREEN?$USER_MESSAGE_ARG={$USER_MESSAGE_ARG}\"\n    const val STATISTICS_ROUTE = STATISTICS_SCREEN\n    const val TASK_DETAIL_ROUTE = \"$TASK_DETAIL_SCREEN/{$TASK_ID_ARG}\"\n    const val ADD_EDIT_TASK_ROUTE = \"$ADD_EDIT_TASK_SCREEN/{$TITLE_ARG}?$TASK_ID_ARG={$TASK_ID_ARG}\"\n}\n\n/**\n * Models the navigation actions in the app.\n */\nclass TodoNavigationActions(private val navController: NavHostController) {\n\n    fun navigateToTasks(userMessage: Int = 0) {\n        val navigatesFromDrawer = userMessage == 0\n        navController.navigate(\n            TASKS_SCREEN.let {\n                if (userMessage != 0) \"$it?$USER_MESSAGE_ARG=$userMessage\" else it\n            }\n        ) {\n            popUpTo(navController.graph.findStartDestination().id) {\n                inclusive = !navigatesFromDrawer\n                saveState = navigatesFromDrawer\n            }\n            launchSingleTop = true\n            restoreState = navigatesFromDrawer\n        }\n    }\n\n    fun navigateToStatistics() {\n        navController.navigate(TodoDestinations.STATISTICS_ROUTE) {\n            // Pop up to the start destination of the graph to\n            // avoid building up a large stack of destinations\n            // on the back stack as users select items\n            popUpTo(navController.graph.findStartDestination().id) {\n                saveState = true\n            }\n            // Avoid multiple copies of the same destination when\n            // reselecting the same item\n            launchSingleTop = true\n            // Restore state when reselecting a previously selected item\n            restoreState = true\n        }\n    }\n\n    fun navigateToTaskDetail(taskId: String) {\n        navController.navigate(\"$TASK_DETAIL_SCREEN/$taskId\")\n    }\n\n    fun navigateToAddEditTask(title: Int, taskId: String?) {\n        navController.navigate(\n            \"$ADD_EDIT_TASK_SCREEN/$title\".let {\n                if (taskId != null) \"$it?$TASK_ID_ARG=$taskId\" else it\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoTheme.kt",
    "content": "package com.example.android.architecture.blueprints.todoapp\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\n\n@Composable\nfun TodoTheme(content: @Composable () -> Unit) {\n    MaterialTheme(\n        colorScheme = lightColorScheme(\n            primary = Color(0xFF263238),\n            secondary = Color(0xFF2E7D32),\n            tertiary = Color(0xFFCCCCCC),\n        )\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage com.example.android.architecture.blueprints.todoapp.addedittask\n\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Done\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SmallFloatingActionButton\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.navigation.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.util.AddEditTaskTopAppBar\n\n@Composable\nfun AddEditTaskScreen(\n    @StringRes topBarTitle: Int,\n    onTaskUpdate: () -> Unit,\n    onBack: () -> Unit,\n    modifier: Modifier = Modifier,\n    viewModel: AddEditTaskViewModel = hiltViewModel(),\n    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }\n) {\n    Scaffold(\n        modifier = modifier.fillMaxSize(),\n        snackbarHost = { SnackbarHost(snackbarHostState) },\n        topBar = { AddEditTaskTopAppBar(topBarTitle, onBack) },\n        floatingActionButton = {\n            SmallFloatingActionButton(onClick = viewModel::saveTask) {\n                Icon(Icons.Filled.Done, stringResource(id = R.string.cd_save_task))\n            }\n        }\n    ) { paddingValues ->\n        val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n\n        AddEditTaskContent(\n            loading = uiState.isLoading,\n            title = uiState.title,\n            description = uiState.description,\n            onTitleChanged = viewModel::updateTitle,\n            onDescriptionChanged = viewModel::updateDescription,\n            modifier = Modifier.padding(paddingValues)\n        )\n\n        // Check if the task is saved and call onTaskUpdate event\n        LaunchedEffect(uiState.isTaskSaved) {\n            if (uiState.isTaskSaved) {\n                onTaskUpdate()\n            }\n        }\n\n        // Check for user messages to display on the screen\n        uiState.userMessage?.let { userMessage ->\n            val snackbarText = stringResource(userMessage)\n            LaunchedEffect(snackbarHostState, viewModel, userMessage, snackbarText) {\n                snackbarHostState.showSnackbar(snackbarText)\n                viewModel.snackbarMessageShown()\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AddEditTaskContent(\n    loading: Boolean,\n    title: String,\n    description: String,\n    onTitleChanged: (String) -> Unit,\n    onDescriptionChanged: (String) -> Unit,\n    modifier: Modifier = Modifier\n) {\n    var isRefreshing by remember { mutableStateOf(false) }\n    val refreshingState = rememberPullToRefreshState()\n    if (loading) {\n        PullToRefreshBox(\n            isRefreshing = isRefreshing,\n            state = refreshingState,\n            onRefresh = { /* DO NOTHING */ },\n            content = { }\n        )\n    } else {\n        Column(\n            modifier\n                .fillMaxWidth()\n                .padding(all = dimensionResource(id = R.dimen.horizontal_margin))\n                .verticalScroll(rememberScrollState())\n        ) {\n            val textFieldColors = OutlinedTextFieldDefaults.colors(\n                focusedBorderColor = Color.Transparent,\n                unfocusedBorderColor = Color.Transparent,\n                cursorColor = MaterialTheme.colorScheme.onSecondary\n            )\n            OutlinedTextField(\n                value = title,\n                modifier = Modifier.fillMaxWidth(),\n                onValueChange = onTitleChanged,\n                placeholder = {\n                    Text(\n                        text = stringResource(id = R.string.title_hint),\n                        style = MaterialTheme.typography.headlineSmall\n                    )\n                },\n                textStyle = MaterialTheme.typography.headlineSmall\n                    .copy(fontWeight = FontWeight.Bold),\n                maxLines = 1,\n                colors = textFieldColors\n            )\n            OutlinedTextField(\n                value = description,\n                onValueChange = onDescriptionChanged,\n                placeholder = { Text(stringResource(id = R.string.description_hint)) },\n                modifier = Modifier\n                    .height(350.dp)\n                    .fillMaxWidth(),\n                colors = textFieldColors\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModel.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.addedittask\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n/**\n * UiState for the Add/Edit screen\n */\ndata class AddEditTaskUiState(\n    val title: String = \"\",\n    val description: String = \"\",\n    val isTaskCompleted: Boolean = false,\n    val isLoading: Boolean = false,\n    val userMessage: Int? = null,\n    val isTaskSaved: Boolean = false\n)\n\n/**\n * ViewModel for the Add/Edit screen.\n */\n@HiltViewModel\nclass AddEditTaskViewModel @Inject constructor(\n    private val taskRepository: TaskRepository,\n    savedStateHandle: SavedStateHandle\n) : ViewModel() {\n\n    private val taskId: String? = savedStateHandle[TodoDestinationsArgs.TASK_ID_ARG]\n\n    // A MutableStateFlow needs to be created in this ViewModel. The source of truth of the current\n    // editable Task is the ViewModel, we need to mutate the UI state directly in methods such as\n    // `updateTitle` or `updateDescription`\n    private val _uiState = MutableStateFlow(AddEditTaskUiState())\n    val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()\n\n    init {\n        if (taskId != null) {\n            loadTask(taskId)\n        }\n    }\n\n    // Called when clicking on fab.\n    fun saveTask() {\n        if (uiState.value.title.isEmpty() || uiState.value.description.isEmpty()) {\n            _uiState.update {\n                it.copy(userMessage = R.string.empty_task_message)\n            }\n            return\n        }\n\n        if (taskId == null) {\n            createNewTask()\n        } else {\n            updateTask()\n        }\n    }\n\n    fun snackbarMessageShown() {\n        _uiState.update {\n            it.copy(userMessage = null)\n        }\n    }\n\n    fun updateTitle(newTitle: String) {\n        _uiState.update {\n            it.copy(title = newTitle)\n        }\n    }\n\n    fun updateDescription(newDescription: String) {\n        _uiState.update {\n            it.copy(description = newDescription)\n        }\n    }\n\n    private fun createNewTask() = viewModelScope.launch {\n        taskRepository.createTask(uiState.value.title, uiState.value.description)\n        _uiState.update {\n            it.copy(isTaskSaved = true)\n        }\n    }\n\n    private fun updateTask() {\n        if (taskId == null) {\n            throw RuntimeException(\"updateTask() was called but task is new.\")\n        }\n        viewModelScope.launch {\n            taskRepository.updateTask(\n                taskId,\n                title = uiState.value.title,\n                description = uiState.value.description,\n            )\n            _uiState.update {\n                it.copy(isTaskSaved = true)\n            }\n        }\n    }\n\n    private fun loadTask(taskId: String) {\n        _uiState.update {\n            it.copy(isLoading = true)\n        }\n        viewModelScope.launch {\n            taskRepository.getTask(taskId).let { task ->\n                if (task != null) {\n                    _uiState.update {\n                        it.copy(\n                            title = task.title,\n                            description = task.description,\n                            isTaskCompleted = task.isCompleted,\n                            isLoading = false\n                        )\n                    }\n                } else {\n                    _uiState.update {\n                        it.copy(isLoading = false)\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepository.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data\n\nimport com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao\nimport com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource\nimport com.example.android.architecture.blueprints.todoapp.di.ApplicationScope\nimport com.example.android.architecture.blueprints.todoapp.di.DefaultDispatcher\nimport kotlinx.coroutines.CoroutineDispatcher\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.util.UUID\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Default implementation of [TaskRepository]. Single entry point for managing tasks' data.\n *\n * @param networkDataSource - The network data source\n * @param localDataSource - The local data source\n * @param dispatcher - The dispatcher to be used for long running or complex operations, such as ID\n * generation or mapping many models.\n * @param scope - The coroutine scope used for deferred jobs where the result isn't important, such\n * as sending data to the network.\n */\n@Singleton\nclass DefaultTaskRepository @Inject constructor(\n    private val networkDataSource: NetworkDataSource,\n    private val localDataSource: TaskDao,\n    @DefaultDispatcher private val dispatcher: CoroutineDispatcher,\n    @ApplicationScope private val scope: CoroutineScope,\n) : TaskRepository {\n\n    override suspend fun createTask(title: String, description: String): String {\n        // ID creation might be a complex operation so it's executed using the supplied\n        // coroutine dispatcher\n        val taskId = withContext(dispatcher) {\n            UUID.randomUUID().toString()\n        }\n        val task = Task(\n            title = title,\n            description = description,\n            id = taskId,\n        )\n        localDataSource.upsert(task.toLocal())\n        saveTasksToNetwork()\n        return taskId\n    }\n\n    override suspend fun updateTask(taskId: String, title: String, description: String) {\n        val task = getTask(taskId)?.copy(\n            title = title,\n            description = description\n        ) ?: throw Exception(\"Task (id $taskId) not found\")\n\n        localDataSource.upsert(task.toLocal())\n        saveTasksToNetwork()\n    }\n\n    override suspend fun getTasks(forceUpdate: Boolean): List<Task> {\n        if (forceUpdate) {\n            refresh()\n        }\n        return withContext(dispatcher) {\n            localDataSource.getAll().toExternal()\n        }\n    }\n\n    override fun getTasksStream(): Flow<List<Task>> {\n        return localDataSource.observeAll().map { tasks ->\n            withContext(dispatcher) {\n                tasks.toExternal()\n            }\n        }\n    }\n\n    override suspend fun refreshTask(taskId: String) {\n        refresh()\n    }\n\n    override fun getTaskStream(taskId: String): Flow<Task?> {\n        return localDataSource.observeById(taskId).map { it.toExternal() }\n    }\n\n    /**\n     * Get a Task with the given ID. Will return null if the task cannot be found.\n     *\n     * @param taskId - The ID of the task\n     * @param forceUpdate - true if the task should be updated from the network data source first.\n     */\n    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Task? {\n        if (forceUpdate) {\n            refresh()\n        }\n        return localDataSource.getById(taskId)?.toExternal()\n    }\n\n    override suspend fun completeTask(taskId: String) {\n        localDataSource.updateCompleted(taskId = taskId, completed = true)\n        saveTasksToNetwork()\n    }\n\n    override suspend fun activateTask(taskId: String) {\n        localDataSource.updateCompleted(taskId = taskId, completed = false)\n        saveTasksToNetwork()\n    }\n\n    override suspend fun clearCompletedTasks() {\n        localDataSource.deleteCompleted()\n        saveTasksToNetwork()\n    }\n\n    override suspend fun deleteAllTasks() {\n        localDataSource.deleteAll()\n        saveTasksToNetwork()\n    }\n\n    override suspend fun deleteTask(taskId: String) {\n        localDataSource.deleteById(taskId)\n        saveTasksToNetwork()\n    }\n\n    /**\n     * The following methods load tasks from (refresh), and save tasks to, the network.\n     *\n     * Real apps may want to do a proper sync, rather than the \"one-way sync everything\" approach\n     * below. See https://developer.android.com/topic/architecture/data-layer/offline-first\n     * for more efficient and robust synchronisation strategies.\n     *\n     * Note that the refresh operation is a suspend function (forces callers to wait) and the save\n     * operation is not. It returns immediately so callers don't have to wait.\n     */\n\n    /**\n     * Delete everything in the local data source and replace it with everything from the network\n     * data source.\n     *\n     * `withContext` is used here in case the bulk `toLocal` mapping operation is complex.\n     */\n    override suspend fun refresh() {\n        withContext(dispatcher) {\n            val remoteTasks = networkDataSource.loadTasks()\n            localDataSource.deleteAll()\n            localDataSource.upsertAll(remoteTasks.toLocal())\n        }\n    }\n\n    /**\n     * Send the tasks from the local data source to the network data source\n     *\n     * Returns immediately after launching the job. Real apps may want to suspend here until the\n     * operation is complete or (better) use WorkManager to schedule this work. Both approaches\n     * should provide a mechanism for failures to be communicated back to the user so that\n     * they are aware that their data isn't being backed up.\n     */\n    private fun saveTasksToNetwork() {\n        scope.launch {\n            try {\n                val localTasks = localDataSource.getAll()\n                val networkTasks = withContext(dispatcher) {\n                    localTasks.toNetwork()\n                }\n                networkDataSource.saveTasks(networkTasks)\n            } catch (e: Exception) {\n                // In a real app you'd handle the exception e.g. by exposing a `networkStatus` flow\n                // to an app level UI state holder which could then display a Toast message.\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/ModelMappingExt.kt",
    "content": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data\n\nimport com.example.android.architecture.blueprints.todoapp.data.source.local.LocalTask\nimport com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkTask\nimport com.example.android.architecture.blueprints.todoapp.data.source.network.TaskStatus\n\n/**\n * Data model mapping extension functions. There are three model types:\n *\n * - Task: External model exposed to other layers in the architecture.\n * Obtained using `toExternal`.\n *\n * - NetworkTask: Internal model used to represent a task from the network. Obtained using\n * `toNetwork`.\n *\n * - LocalTask: Internal model used to represent a task stored locally in a database. Obtained\n * using `toLocal`.\n *\n */\n\n// External to local\nfun Task.toLocal() = LocalTask(\n    id = id,\n    title = title,\n    description = description,\n    isCompleted = isCompleted,\n)\n\nfun List<Task>.toLocal() = map(Task::toLocal)\n\n// Local to External\nfun LocalTask.toExternal() = Task(\n    id = id,\n    title = title,\n    description = description,\n    isCompleted = isCompleted,\n)\n\n// Note: JvmName is used to provide a unique name for each extension function with the same name.\n// Without this, type erasure will cause compiler errors because these methods will have the same\n// signature on the JVM.\n@JvmName(\"localToExternal\")\nfun List<LocalTask>.toExternal() = map(LocalTask::toExternal)\n\n// Network to Local\nfun NetworkTask.toLocal() = LocalTask(\n    id = id,\n    title = title,\n    description = shortDescription,\n    isCompleted = (status == TaskStatus.COMPLETE),\n)\n\n@JvmName(\"networkToLocal\")\nfun List<NetworkTask>.toLocal() = map(NetworkTask::toLocal)\n\n// Local to Network\nfun LocalTask.toNetwork() = NetworkTask(\n    id = id,\n    title = title,\n    shortDescription = description,\n    status = if (isCompleted) { TaskStatus.COMPLETE } else { TaskStatus.ACTIVE }\n)\n\nfun List<LocalTask>.toNetwork() = map(LocalTask::toNetwork)\n\n// External to Network\nfun Task.toNetwork() = toLocal().toNetwork()\n\n@JvmName(\"externalToNetwork\")\nfun List<Task>.toNetwork() = map(Task::toNetwork)\n\n// Network to External\nfun NetworkTask.toExternal() = toLocal().toExternal()\n\n@JvmName(\"networkToExternal\")\nfun List<NetworkTask>.toExternal() = map(NetworkTask::toExternal)\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/Task.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data\n\n/**\n * Immutable model class for a Task.\n *\n * @param title title of the task\n * @param description description of the task\n * @param isCompleted whether or not this task is completed\n * @param id id of the task\n *\n * TODO: The constructor of this class should be `internal` but it is used in previews and tests\n *  so that's not possible until those previews/tests are refactored.\n */\ndata class Task(\n    val title: String = \"\",\n    val description: String = \"\",\n    val isCompleted: Boolean = false,\n    val id: String,\n) {\n\n    val titleForList: String\n        get() = if (title.isNotEmpty()) title else description\n\n    val isActive\n        get() = !isCompleted\n\n    val isEmpty\n        get() = title.isEmpty() || description.isEmpty()\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/TaskRepository.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data\n\nimport kotlinx.coroutines.flow.Flow\n\n/**\n * Interface to the data layer.\n */\ninterface TaskRepository {\n\n    fun getTasksStream(): Flow<List<Task>>\n\n    suspend fun getTasks(forceUpdate: Boolean = false): List<Task>\n\n    suspend fun refresh()\n\n    fun getTaskStream(taskId: String): Flow<Task?>\n\n    suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Task?\n\n    suspend fun refreshTask(taskId: String)\n\n    suspend fun createTask(title: String, description: String): String\n\n    suspend fun updateTask(taskId: String, title: String, description: String)\n\n    suspend fun completeTask(taskId: String)\n\n    suspend fun activateTask(taskId: String)\n\n    suspend fun clearCompletedTasks()\n\n    suspend fun deleteAllTasks()\n\n    suspend fun deleteTask(taskId: String)\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/LocalTask.kt",
    "content": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.local\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n/**\n * Internal model used to represent a task stored locally in a Room database. This is used inside\n * the data layer only.\n *\n * See ModelMappingExt.kt for mapping functions used to convert this model to other\n * models.\n */\n@Entity(\n    tableName = \"task\"\n)\ndata class LocalTask(\n    @PrimaryKey val id: String,\n    var title: String,\n    var description: String,\n    var isCompleted: Boolean,\n)\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/TaskDao.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.local\n\nimport androidx.room.Dao\nimport androidx.room.Query\nimport androidx.room.Upsert\nimport kotlinx.coroutines.flow.Flow\n\n/**\n * Data Access Object for the task table.\n */\n@Dao\ninterface TaskDao {\n\n    /**\n     * Observes list of tasks.\n     *\n     * @return all tasks.\n     */\n    @Query(\"SELECT * FROM task\")\n    fun observeAll(): Flow<List<LocalTask>>\n\n    /**\n     * Observes a single task.\n     *\n     * @param taskId the task id.\n     * @return the task with taskId.\n     */\n    @Query(\"SELECT * FROM task WHERE id = :taskId\")\n    fun observeById(taskId: String): Flow<LocalTask>\n\n    /**\n     * Select all tasks from the tasks table.\n     *\n     * @return all tasks.\n     */\n    @Query(\"SELECT * FROM task\")\n    suspend fun getAll(): List<LocalTask>\n\n    /**\n     * Select a task by id.\n     *\n     * @param taskId the task id.\n     * @return the task with taskId.\n     */\n    @Query(\"SELECT * FROM task WHERE id = :taskId\")\n    suspend fun getById(taskId: String): LocalTask?\n\n    /**\n     * Insert or update a task in the database. If a task already exists, replace it.\n     *\n     * @param task the task to be inserted or updated.\n     */\n    @Upsert\n    suspend fun upsert(task: LocalTask)\n\n    /**\n     * Insert or update tasks in the database. If a task already exists, replace it.\n     *\n     * @param tasks the tasks to be inserted or updated.\n     */\n    @Upsert\n    suspend fun upsertAll(tasks: List<LocalTask>)\n\n    /**\n     * Update the complete status of a task\n     *\n     * @param taskId id of the task\n     * @param completed status to be updated\n     */\n    @Query(\"UPDATE task SET isCompleted = :completed WHERE id = :taskId\")\n    suspend fun updateCompleted(taskId: String, completed: Boolean)\n\n    /**\n     * Delete a task by id.\n     *\n     * @return the number of tasks deleted. This should always be 1.\n     */\n    @Query(\"DELETE FROM task WHERE id = :taskId\")\n    suspend fun deleteById(taskId: String): Int\n\n    /**\n     * Delete all tasks.\n     */\n    @Query(\"DELETE FROM task\")\n    suspend fun deleteAll()\n\n    /**\n     * Delete all completed tasks from the table.\n     *\n     * @return the number of tasks deleted.\n     */\n    @Query(\"DELETE FROM task WHERE isCompleted = 1\")\n    suspend fun deleteCompleted(): Int\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/ToDoDatabase.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.local\n\nimport androidx.room.Database\nimport androidx.room.RoomDatabase\n\n/**\n * The Room Database that contains the Task table.\n *\n * Note that exportSchema should be true in production databases.\n */\n@Database(entities = [LocalTask::class], version = 1, exportSchema = false)\nabstract class ToDoDatabase : RoomDatabase() {\n\n    abstract fun taskDao(): TaskDao\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkDataSource.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.network\n\n/**\n * Main entry point for accessing tasks data from the network.\n *\n */\ninterface NetworkDataSource {\n\n    suspend fun loadTasks(): List<NetworkTask>\n\n    suspend fun saveTasks(tasks: List<NetworkTask>)\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/NetworkTask.kt",
    "content": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.network\n\n/**\n * Internal model used to represent a task obtained from the network. This is used inside the data\n * layer only.\n *\n * See ModelMappingExt.kt for mapping functions used to convert this model to other\n * models.\n */\ndata class NetworkTask(\n    val id: String,\n    val title: String,\n    val shortDescription: String,\n    val priority: Int? = null,\n    val status: TaskStatus = TaskStatus.ACTIVE\n)\n\nenum class TaskStatus {\n    ACTIVE,\n    COMPLETE\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/TaskNetworkDataSource.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.network\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport javax.inject.Inject\n\nclass TaskNetworkDataSource @Inject constructor() : NetworkDataSource {\n\n    // A mutex is used to ensure that reads and writes are thread-safe.\n    private val accessMutex = Mutex()\n    private var tasks = listOf(\n        NetworkTask(\n            id = \"PISA\",\n            title = \"Build tower in Pisa\",\n            shortDescription = \"Ground looks good, no foundation work required.\"\n        ),\n        NetworkTask(\n            id = \"TACOMA\",\n            title = \"Finish bridge in Tacoma\",\n            shortDescription = \"Found awesome girders at half the cost!\"\n        )\n    )\n\n    override suspend fun loadTasks(): List<NetworkTask> = accessMutex.withLock {\n        delay(SERVICE_LATENCY_IN_MILLIS)\n        return tasks\n    }\n\n    override suspend fun saveTasks(newTasks: List<NetworkTask>) = accessMutex.withLock {\n        delay(SERVICE_LATENCY_IN_MILLIS)\n        tasks = newTasks\n    }\n}\n\nprivate const val SERVICE_LATENCY_IN_MILLIS = 2000L\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/CoroutinesModule.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.di\n\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.components.SingletonComponent\nimport kotlinx.coroutines.CoroutineDispatcher\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport javax.inject.Qualifier\nimport javax.inject.Singleton\n\n@Qualifier\n@Retention(AnnotationRetention.RUNTIME)\nannotation class IoDispatcher\n\n@Retention(AnnotationRetention.RUNTIME)\n@Qualifier\nannotation class DefaultDispatcher\n\n@Retention(AnnotationRetention.RUNTIME)\n@Qualifier\nannotation class ApplicationScope\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject CoroutinesModule {\n\n    @Provides\n    @IoDispatcher\n    fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO\n\n    @Provides\n    @DefaultDispatcher\n    fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default\n\n    @Provides\n    @Singleton\n    @ApplicationScope\n    fun providesCoroutineScope(\n        @DefaultDispatcher dispatcher: CoroutineDispatcher\n    ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DataModules.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.di\n\nimport android.content.Context\nimport androidx.room.Room\nimport com.example.android.architecture.blueprints.todoapp.data.DefaultTaskRepository\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport com.example.android.architecture.blueprints.todoapp.data.source.local.TaskDao\nimport com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase\nimport com.example.android.architecture.blueprints.todoapp.data.source.network.NetworkDataSource\nimport com.example.android.architecture.blueprints.todoapp.data.source.network.TaskNetworkDataSource\nimport dagger.Binds\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nabstract class RepositoryModule {\n\n    @Singleton\n    @Binds\n    abstract fun bindTaskRepository(repository: DefaultTaskRepository): TaskRepository\n}\n\n@Module\n@InstallIn(SingletonComponent::class)\nabstract class DataSourceModule {\n\n    @Singleton\n    @Binds\n    abstract fun bindNetworkDataSource(dataSource: TaskNetworkDataSource): NetworkDataSource\n}\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject DatabaseModule {\n\n    @Singleton\n    @Provides\n    fun provideDataBase(@ApplicationContext context: Context): ToDoDatabase {\n        return Room.databaseBuilder(\n            context.applicationContext,\n            ToDoDatabase::class.java,\n            \"Tasks.db\"\n        ).build()\n    }\n\n    @Provides\n    fun provideTaskDao(database: ToDoDatabase): TaskDao = database.taskDao()\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsScreen.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.statistics\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.hilt.navigation.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.util.LoadingContent\nimport com.example.android.architecture.blueprints.todoapp.util.StatisticsTopAppBar\n\n@Composable\nfun StatisticsScreen(\n    openDrawer: () -> Unit,\n    modifier: Modifier = Modifier,\n    viewModel: StatisticsViewModel = hiltViewModel(),\n    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }\n) {\n    Scaffold(\n        modifier = modifier.fillMaxSize(),\n        snackbarHost = { SnackbarHost(snackbarHostState) },\n        topBar = { StatisticsTopAppBar(openDrawer) },\n    ) { paddingValues ->\n        val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n\n        StatisticsContent(\n            loading = uiState.isLoading,\n            empty = uiState.isEmpty,\n            activeTasksPercent = uiState.activeTasksPercent,\n            completedTasksPercent = uiState.completedTasksPercent,\n            onRefresh = { viewModel.refresh() },\n            modifier = modifier.padding(paddingValues)\n        )\n    }\n}\n\n@Composable\nprivate fun StatisticsContent(\n    loading: Boolean,\n    empty: Boolean,\n    activeTasksPercent: Float,\n    completedTasksPercent: Float,\n    onRefresh: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    val commonModifier = modifier\n        .fillMaxSize()\n        .padding(all = dimensionResource(id = R.dimen.horizontal_margin))\n\n    LoadingContent(\n        loading = loading,\n        empty = empty,\n        onRefresh = onRefresh,\n        modifier = modifier,\n        emptyContent = {\n            Text(\n                text = stringResource(id = R.string.statistics_no_tasks),\n                modifier = commonModifier\n            )\n        }\n    ) {\n        Column(\n            commonModifier\n                .fillMaxSize()\n                .verticalScroll(rememberScrollState())\n        ) {\n            if (!loading) {\n                Text(stringResource(id = R.string.statistics_active_tasks, activeTasksPercent))\n                Text(\n                    stringResource(\n                        id = R.string.statistics_completed_tasks,\n                        completedTasksPercent\n                    )\n                )\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nfun StatisticsContentPreview() {\n    Surface {\n        StatisticsContent(\n            loading = false,\n            empty = false,\n            activeTasksPercent = 80f,\n            completedTasksPercent = 20f,\n            onRefresh = { }\n        )\n    }\n}\n\n@Preview\n@Composable\nfun StatisticsContentEmptyPreview() {\n    Surface {\n        StatisticsScreen({})\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtils.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.statistics\n\nimport com.example.android.architecture.blueprints.todoapp.data.Task\n\n/**\n * Function that does some trivial computation. Used to showcase unit tests.\n */\ninternal fun getActiveAndCompletedStats(tasks: List<Task>): StatsResult {\n\n    return if (tasks.isEmpty()) {\n        StatsResult(0f, 0f)\n    } else {\n        val totalTasks = tasks.size\n        val numberOfActiveTasks = tasks.count { it.isActive }\n        StatsResult(\n            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,\n            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size\n        )\n    }\n}\n\ndata class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModel.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.statistics\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport com.example.android.architecture.blueprints.todoapp.util.Async\nimport com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.catch\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n/**\n * UiState for the statistics screen.\n */\ndata class StatisticsUiState(\n    val isEmpty: Boolean = false,\n    val isLoading: Boolean = false,\n    val activeTasksPercent: Float = 0f,\n    val completedTasksPercent: Float = 0f\n)\n\n/**\n * ViewModel for the statistics screen.\n */\n@HiltViewModel\nclass StatisticsViewModel @Inject constructor(\n    private val taskRepository: TaskRepository\n) : ViewModel() {\n\n    val uiState: StateFlow<StatisticsUiState> =\n        taskRepository.getTasksStream()\n            .map { Async.Success(it) }\n            .catch<Async<List<Task>>> { emit(Async.Error(R.string.loading_tasks_error)) }\n            .map { taskAsync -> produceStatisticsUiState(taskAsync) }\n            .stateIn(\n                scope = viewModelScope,\n                started = WhileUiSubscribed,\n                initialValue = StatisticsUiState(isLoading = true)\n            )\n\n    fun refresh() {\n        viewModelScope.launch {\n            taskRepository.refresh()\n        }\n    }\n\n    private fun produceStatisticsUiState(taskLoad: Async<List<Task>>) =\n        when (taskLoad) {\n            Async.Loading -> {\n                StatisticsUiState(isLoading = true, isEmpty = true)\n            }\n            is Async.Error -> {\n                // TODO: Show error message?\n                StatisticsUiState(isEmpty = true, isLoading = false)\n            }\n            is Async.Success -> {\n                val stats = getActiveAndCompletedStats(taskLoad.data)\n                StatisticsUiState(\n                    isEmpty = taskLoad.data.isEmpty(),\n                    activeTasksPercent = stats.activeTasksPercent,\n                    completedTasksPercent = stats.completedTasksPercent,\n                    isLoading = false\n                )\n            }\n        }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailScreen.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.taskdetail\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SmallFloatingActionButton\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.hilt.navigation.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.example.android.architecture.blueprints.todoapp.util.LoadingContent\nimport com.example.android.architecture.blueprints.todoapp.util.TaskDetailTopAppBar\n\n@Composable\nfun TaskDetailScreen(\n    onEditTask: (String) -> Unit,\n    onBack: () -> Unit,\n    onDeleteTask: () -> Unit,\n    modifier: Modifier = Modifier,\n    viewModel: TaskDetailViewModel = hiltViewModel(),\n    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }\n\n) {\n    Scaffold(\n        modifier = modifier.fillMaxSize(),\n        snackbarHost = { SnackbarHost(snackbarHostState) },\n        topBar = { TaskDetailTopAppBar(onBack = onBack, onDelete = viewModel::deleteTask) },\n        floatingActionButton = {\n            SmallFloatingActionButton(onClick = { onEditTask(viewModel.taskId) }) {\n                Icon(Icons.Filled.Edit, stringResource(id = R.string.edit_task))\n            }\n        }\n    ) { paddingValues ->\n        val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n\n        EditTaskContent(\n            loading = uiState.isLoading,\n            empty = uiState.task == null && !uiState.isLoading,\n            task = uiState.task,\n            onRefresh = viewModel::refresh,\n            onTaskCheck = viewModel::setCompleted,\n            modifier = Modifier.padding(paddingValues)\n        )\n\n        // Check for user messages to display on the screen\n        uiState.userMessage?.let { userMessage ->\n            val snackbarText = stringResource(userMessage)\n            LaunchedEffect(snackbarHostState, viewModel, userMessage, snackbarText) {\n                snackbarHostState.showSnackbar(snackbarText)\n                viewModel.snackbarMessageShown()\n            }\n        }\n\n        // Check if the task is deleted and call onDeleteTask\n        LaunchedEffect(uiState.isTaskDeleted) {\n            if (uiState.isTaskDeleted) {\n                onDeleteTask()\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun EditTaskContent(\n    loading: Boolean,\n    empty: Boolean,\n    task: Task?,\n    onTaskCheck: (Boolean) -> Unit,\n    onRefresh: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    val screenPadding = Modifier.padding(\n        horizontal = dimensionResource(id = R.dimen.horizontal_margin),\n        vertical = dimensionResource(id = R.dimen.vertical_margin),\n    )\n    val commonModifier = modifier\n        .fillMaxWidth()\n        .then(screenPadding)\n\n    LoadingContent(\n        loading = loading,\n        empty = empty,\n        emptyContent = {\n            Text(\n                text = stringResource(id = R.string.no_data),\n                modifier = commonModifier\n            )\n        },\n        onRefresh = onRefresh\n    ) {\n        Column(commonModifier.verticalScroll(rememberScrollState())) {\n            Row(\n                Modifier\n                    .fillMaxWidth()\n                    .then(screenPadding),\n\n            ) {\n                if (task != null) {\n                    Checkbox(task.isCompleted, onTaskCheck)\n                    Column {\n                        Text(text = task.title, style = MaterialTheme.typography.headlineSmall)\n                        Text(text = task.description, style = MaterialTheme.typography.bodySmall)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun EditTaskContentPreview() {\n    Surface {\n        EditTaskContent(\n            loading = false,\n            empty = false,\n            Task(\n                title = \"Title\",\n                description = \"Description\",\n                isCompleted = false,\n                id = \"ID\"\n            ),\n            onTaskCheck = { },\n            onRefresh = { }\n        )\n    }\n\n}\n\n@Preview\n@Composable\nprivate fun EditTaskContentTaskCompletedPreview() {\n    Surface {\n        EditTaskContent(\n            loading = false,\n            empty = false,\n            Task(\n                title = \"Title\",\n                description = \"Description\",\n                isCompleted = false,\n                id = \"ID\"\n            ),\n            onTaskCheck = { },\n            onRefresh = { }\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun EditTaskContentEmptyPreview() {\n    Surface {\n        EditTaskContent(\n            loading = false,\n            empty = true,\n            Task(\n                title = \"Title\",\n                description = \"Description\",\n                isCompleted = false,\n                id = \"ID\"\n            ),\n            onTaskCheck = { },\n            onRefresh = { }\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModel.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.taskdetail\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport com.example.android.architecture.blueprints.todoapp.util.Async\nimport com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.catch\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n/**\n * UiState for the Details screen.\n */\ndata class TaskDetailUiState(\n    val task: Task? = null,\n    val isLoading: Boolean = false,\n    val userMessage: Int? = null,\n    val isTaskDeleted: Boolean = false\n)\n\n/**\n * ViewModel for the Details screen.\n */\n@HiltViewModel\nclass TaskDetailViewModel @Inject constructor(\n    private val taskRepository: TaskRepository,\n    savedStateHandle: SavedStateHandle\n) : ViewModel() {\n\n    val taskId: String = savedStateHandle[TodoDestinationsArgs.TASK_ID_ARG]!!\n\n    private val _userMessage: MutableStateFlow<Int?> = MutableStateFlow(null)\n    private val _isLoading = MutableStateFlow(false)\n    private val _isTaskDeleted = MutableStateFlow(false)\n    private val _taskAsync = taskRepository.getTaskStream(taskId)\n        .map { handleTask(it) }\n        .catch { emit(Async.Error(R.string.loading_task_error)) }\n\n    val uiState: StateFlow<TaskDetailUiState> = combine(\n        _userMessage, _isLoading, _isTaskDeleted, _taskAsync\n    ) { userMessage, isLoading, isTaskDeleted, taskAsync ->\n        when (taskAsync) {\n            Async.Loading -> {\n                TaskDetailUiState(isLoading = true)\n            }\n            is Async.Error -> {\n                TaskDetailUiState(\n                    userMessage = taskAsync.errorMessage,\n                    isTaskDeleted = isTaskDeleted\n                )\n            }\n            is Async.Success -> {\n                TaskDetailUiState(\n                    task = taskAsync.data,\n                    isLoading = isLoading,\n                    userMessage = userMessage,\n                    isTaskDeleted = isTaskDeleted\n                )\n            }\n        }\n    }\n        .stateIn(\n            scope = viewModelScope,\n            started = WhileUiSubscribed,\n            initialValue = TaskDetailUiState(isLoading = true)\n        )\n\n    fun deleteTask() = viewModelScope.launch {\n        taskRepository.deleteTask(taskId)\n        _isTaskDeleted.value = true\n    }\n\n    fun setCompleted(completed: Boolean) = viewModelScope.launch {\n        val task = uiState.value.task ?: return@launch\n        if (completed) {\n            taskRepository.completeTask(task.id)\n            showSnackbarMessage(R.string.task_marked_complete)\n        } else {\n            taskRepository.activateTask(task.id)\n            showSnackbarMessage(R.string.task_marked_active)\n        }\n    }\n\n    fun refresh() {\n        _isLoading.value = true\n        viewModelScope.launch {\n            taskRepository.refreshTask(taskId)\n            _isLoading.value = false\n        }\n    }\n\n    fun snackbarMessageShown() {\n        _userMessage.value = null\n    }\n\n    private fun showSnackbarMessage(message: Int) {\n        _userMessage.value = message\n    }\n\n    private fun handleTask(task: Task?): Async<Task?> {\n        if (task == null) {\n            return Async.Error(R.string.task_not_found)\n        }\n        return Async.Success(task)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFilterType.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.tasks\n\n/**\n * Used with the filter spinner in the tasks list.\n */\nenum class TasksFilterType {\n    /**\n     * Do not filter tasks.\n     */\n    ALL_TASKS,\n\n    /**\n     * Filters only the active (not completed yet) tasks.\n     */\n    ACTIVE_TASKS,\n\n    /**\n     * Filters only the completed tasks.\n     */\n    COMPLETED_TASKS\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreen.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.tasks\n\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SmallFloatingActionButton\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.navigation.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ACTIVE_TASKS\nimport com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ALL_TASKS\nimport com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.COMPLETED_TASKS\nimport com.example.android.architecture.blueprints.todoapp.util.LoadingContent\nimport com.example.android.architecture.blueprints.todoapp.util.TasksTopAppBar\n\n@Composable\nfun TasksScreen(\n    @StringRes userMessage: Int,\n    onAddTask: () -> Unit,\n    onTaskClick: (Task) -> Unit,\n    onUserMessageDisplayed: () -> Unit,\n    openDrawer: () -> Unit,\n    modifier: Modifier = Modifier,\n    viewModel: TasksViewModel = hiltViewModel(),\n    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }\n) {\n    Scaffold(\n        modifier = modifier.fillMaxSize(),\n        snackbarHost = { SnackbarHost(snackbarHostState) },\n        topBar = {\n            TasksTopAppBar(\n                openDrawer = openDrawer,\n                onFilterAllTasks = { viewModel.setFiltering(ALL_TASKS) },\n                onFilterActiveTasks = { viewModel.setFiltering(ACTIVE_TASKS) },\n                onFilterCompletedTasks = { viewModel.setFiltering(COMPLETED_TASKS) },\n                onClearCompletedTasks = { viewModel.clearCompletedTasks() },\n                onRefresh = { viewModel.refresh() }\n            )\n        },\n        floatingActionButton = {\n            SmallFloatingActionButton(onClick = onAddTask) {\n                Icon(Icons.Filled.Add, stringResource(id = R.string.add_task))\n            }\n        }\n    ) { paddingValues ->\n        val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n\n        TasksContent(\n            loading = uiState.isLoading,\n            tasks = uiState.items,\n            currentFilteringLabel = uiState.filteringUiInfo.currentFilteringLabel,\n            noTasksLabel = uiState.filteringUiInfo.noTasksLabel,\n            noTasksIconRes = uiState.filteringUiInfo.noTaskIconRes,\n            onRefresh = viewModel::refresh,\n            onTaskClick = onTaskClick,\n            onTaskCheckedChange = viewModel::completeTask,\n            modifier = Modifier.padding(paddingValues)\n        )\n\n        // Check for user messages to display on the screen\n        uiState.userMessage?.let { message ->\n            val snackbarText = stringResource(message)\n            LaunchedEffect(snackbarHostState, viewModel, message, snackbarText) {\n                snackbarHostState.showSnackbar(snackbarText)\n                viewModel.snackbarMessageShown()\n            }\n        }\n\n        // Check if there's a userMessage to show to the user\n        val currentOnUserMessageDisplayed by rememberUpdatedState(onUserMessageDisplayed)\n        LaunchedEffect(userMessage) {\n            if (userMessage != 0) {\n                viewModel.showEditResultMessage(userMessage)\n                currentOnUserMessageDisplayed()\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TasksContent(\n    loading: Boolean,\n    tasks: List<Task>,\n    @StringRes currentFilteringLabel: Int,\n    @StringRes noTasksLabel: Int,\n    @DrawableRes noTasksIconRes: Int,\n    onRefresh: () -> Unit,\n    onTaskClick: (Task) -> Unit,\n    onTaskCheckedChange: (Task, Boolean) -> Unit,\n    modifier: Modifier = Modifier\n) {\n    LoadingContent(\n        loading = loading,\n        empty = tasks.isEmpty() && !loading,\n        emptyContent = { TasksEmptyContent(noTasksLabel, noTasksIconRes, modifier) },\n        onRefresh = onRefresh\n    ) {\n        Column(\n            modifier = modifier\n                .fillMaxSize()\n                .padding(horizontal = dimensionResource(id = R.dimen.horizontal_margin))\n        ) {\n            Text(\n                text = stringResource(currentFilteringLabel),\n                modifier = Modifier.padding(\n                    horizontal = dimensionResource(id = R.dimen.list_item_padding),\n                    vertical = dimensionResource(id = R.dimen.vertical_margin)\n                ),\n                style = MaterialTheme.typography.headlineSmall\n            )\n            LazyColumn {\n                items(tasks) { task ->\n                    TaskItem(\n                        task = task,\n                        onTaskClick = onTaskClick,\n                        onCheckedChange = { onTaskCheckedChange(task, it) }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TaskItem(\n    task: Task,\n    onCheckedChange: (Boolean) -> Unit,\n    onTaskClick: (Task) -> Unit\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(\n                horizontal = dimensionResource(id = R.dimen.horizontal_margin),\n                vertical = dimensionResource(id = R.dimen.list_item_padding),\n            )\n            .clickable { onTaskClick(task) }\n    ) {\n        Checkbox(\n            checked = task.isCompleted,\n            onCheckedChange = onCheckedChange\n        )\n        Text(\n            text = task.titleForList,\n            style = MaterialTheme.typography.headlineSmall,\n            modifier = Modifier.padding(\n                start = dimensionResource(id = R.dimen.horizontal_margin)\n            ),\n            textDecoration = if (task.isCompleted) {\n                TextDecoration.LineThrough\n            } else {\n                null\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun TasksEmptyContent(\n    @StringRes noTasksLabel: Int,\n    @DrawableRes noTasksIconRes: Int,\n    modifier: Modifier = Modifier\n) {\n    Column(\n        modifier = modifier.fillMaxSize(),\n        verticalArrangement = Arrangement.Center,\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Image(\n            painter = painterResource(id = noTasksIconRes),\n            contentDescription = stringResource(R.string.no_tasks_image_content_description),\n            modifier = Modifier.size(96.dp)\n        )\n        Text(stringResource(id = noTasksLabel))\n    }\n}\n\n@Preview\n@Composable\nprivate fun TasksContentPreview() {\n    MaterialTheme {\n        Surface {\n            TasksContent(\n                loading = false,\n                tasks = listOf(\n                    Task(\n                        title = \"Title 1\",\n                        description = \"Description 1\",\n                        isCompleted = false,\n                        id = \"ID 1\"\n                    ),\n                    Task(\n                        title = \"Title 2\",\n                        description = \"Description 2\",\n                        isCompleted = true,\n                        id = \"ID 2\"\n                    ),\n                    Task(\n                        title = \"Title 3\",\n                        description = \"Description 3\",\n                        isCompleted = true,\n                        id = \"ID 3\"\n                    ),\n                    Task(\n                        title = \"Title 4\",\n                        description = \"Description 4\",\n                        isCompleted = false,\n                        id = \"ID 4\"\n                    ),\n                    Task(\n                        title = \"Title 5\",\n                        description = \"Description 5\",\n                        isCompleted = true,\n                        id = \"ID 5\"\n                    ),\n                ),\n                currentFilteringLabel = R.string.label_all,\n                noTasksLabel = R.string.no_tasks_all,\n                noTasksIconRes = R.drawable.logo_no_fill,\n                onRefresh = { },\n                onTaskClick = { },\n                onTaskCheckedChange = { _, _ -> },\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun TasksContentEmptyPreview() {\n    MaterialTheme {\n        Surface {\n            TasksContent(\n                loading = false,\n                tasks = emptyList(),\n                currentFilteringLabel = R.string.label_all,\n                noTasksLabel = R.string.no_tasks_all,\n                noTasksIconRes = R.drawable.logo_no_fill,\n                onRefresh = { },\n                onTaskClick = { },\n                onTaskCheckedChange = { _, _ -> },\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun TasksEmptyContentPreview() {\n    TodoTheme {\n        Surface {\n            TasksEmptyContent(\n                noTasksLabel = R.string.no_tasks_all,\n                noTasksIconRes = R.drawable.logo_no_fill\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun TaskItemPreview() {\n    MaterialTheme {\n        Surface {\n            TaskItem(\n                task = Task(\n                    title = \"Title\",\n                    description = \"Description\",\n                    id = \"ID\"\n                ),\n                onTaskClick = { },\n                onCheckedChange = { }\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun TaskItemCompletedPreview() {\n    MaterialTheme {\n        Surface {\n            TaskItem(\n                task = Task(\n                    title = \"Title\",\n                    description = \"Description\",\n                    isCompleted = true,\n                    id = \"ID\"\n                ),\n                onTaskClick = { },\n                onCheckedChange = { }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModel.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.tasks\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.example.android.architecture.blueprints.todoapp.ADD_EDIT_RESULT_OK\nimport com.example.android.architecture.blueprints.todoapp.DELETE_RESULT_OK\nimport com.example.android.architecture.blueprints.todoapp.EDIT_RESULT_OK\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ACTIVE_TASKS\nimport com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.ALL_TASKS\nimport com.example.android.architecture.blueprints.todoapp.tasks.TasksFilterType.COMPLETED_TASKS\nimport com.example.android.architecture.blueprints.todoapp.util.Async\nimport com.example.android.architecture.blueprints.todoapp.util.WhileUiSubscribed\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.catch\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n/**\n * UiState for the task list screen.\n */\ndata class TasksUiState(\n    val items: List<Task> = emptyList(),\n    val isLoading: Boolean = false,\n    val filteringUiInfo: FilteringUiInfo = FilteringUiInfo(),\n    val userMessage: Int? = null\n)\n\n/**\n * ViewModel for the task list screen.\n */\n@HiltViewModel\nclass TasksViewModel @Inject constructor(\n    private val taskRepository: TaskRepository,\n    private val savedStateHandle: SavedStateHandle\n) : ViewModel() {\n\n    private val _savedFilterType =\n        savedStateHandle.getStateFlow(TASKS_FILTER_SAVED_STATE_KEY, ALL_TASKS)\n\n    private val _filterUiInfo = _savedFilterType.map { getFilterUiInfo(it) }.distinctUntilChanged()\n    private val _userMessage: MutableStateFlow<Int?> = MutableStateFlow(null)\n    private val _isLoading = MutableStateFlow(false)\n    private val _filteredTasksAsync =\n        combine(taskRepository.getTasksStream(), _savedFilterType) { tasks, type ->\n            filterTasks(tasks, type)\n        }\n            .map { Async.Success(it) }\n            .catch<Async<List<Task>>> { emit(Async.Error(R.string.loading_tasks_error)) }\n\n    val uiState: StateFlow<TasksUiState> = combine(\n        _filterUiInfo, _isLoading, _userMessage, _filteredTasksAsync\n    ) { filterUiInfo, isLoading, userMessage, tasksAsync ->\n        when (tasksAsync) {\n            Async.Loading -> {\n                TasksUiState(isLoading = true)\n            }\n            is Async.Error -> {\n                TasksUiState(userMessage = tasksAsync.errorMessage)\n            }\n            is Async.Success -> {\n                TasksUiState(\n                    items = tasksAsync.data,\n                    filteringUiInfo = filterUiInfo,\n                    isLoading = isLoading,\n                    userMessage = userMessage\n                )\n            }\n        }\n    }\n        .stateIn(\n            scope = viewModelScope,\n            started = WhileUiSubscribed,\n            initialValue = TasksUiState(isLoading = true)\n        )\n\n    fun setFiltering(requestType: TasksFilterType) {\n        savedStateHandle[TASKS_FILTER_SAVED_STATE_KEY] = requestType\n    }\n\n    fun clearCompletedTasks() {\n        viewModelScope.launch {\n            taskRepository.clearCompletedTasks()\n            showSnackbarMessage(R.string.completed_tasks_cleared)\n            refresh()\n        }\n    }\n\n    fun completeTask(task: Task, completed: Boolean) = viewModelScope.launch {\n        if (completed) {\n            taskRepository.completeTask(task.id)\n            showSnackbarMessage(R.string.task_marked_complete)\n        } else {\n            taskRepository.activateTask(task.id)\n            showSnackbarMessage(R.string.task_marked_active)\n        }\n    }\n\n    fun showEditResultMessage(result: Int) {\n        when (result) {\n            EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_saved_task_message)\n            ADD_EDIT_RESULT_OK -> showSnackbarMessage(R.string.successfully_added_task_message)\n            DELETE_RESULT_OK -> showSnackbarMessage(R.string.successfully_deleted_task_message)\n        }\n    }\n\n    fun snackbarMessageShown() {\n        _userMessage.value = null\n    }\n\n    private fun showSnackbarMessage(message: Int) {\n        _userMessage.value = message\n    }\n\n    fun refresh() {\n        _isLoading.value = true\n        viewModelScope.launch {\n            taskRepository.refresh()\n            _isLoading.value = false\n        }\n    }\n\n    private fun filterTasks(tasks: List<Task>, filteringType: TasksFilterType): List<Task> {\n        val tasksToShow = ArrayList<Task>()\n        // We filter the tasks based on the requestType\n        for (task in tasks) {\n            when (filteringType) {\n                ALL_TASKS -> tasksToShow.add(task)\n                ACTIVE_TASKS -> if (task.isActive) {\n                    tasksToShow.add(task)\n                }\n                COMPLETED_TASKS -> if (task.isCompleted) {\n                    tasksToShow.add(task)\n                }\n            }\n        }\n        return tasksToShow\n    }\n\n    private fun getFilterUiInfo(requestType: TasksFilterType): FilteringUiInfo =\n        when (requestType) {\n            ALL_TASKS -> {\n                FilteringUiInfo(\n                    R.string.label_all, R.string.no_tasks_all,\n                    R.drawable.logo_no_fill\n                )\n            }\n            ACTIVE_TASKS -> {\n                FilteringUiInfo(\n                    R.string.label_active, R.string.no_tasks_active,\n                    R.drawable.ic_check_circle_96dp\n                )\n            }\n            COMPLETED_TASKS -> {\n                FilteringUiInfo(\n                    R.string.label_completed, R.string.no_tasks_completed,\n                    R.drawable.ic_verified_user_96dp\n                )\n            }\n        }\n}\n\n// Used to save the current filtering in SavedStateHandle.\nconst val TASKS_FILTER_SAVED_STATE_KEY = \"TASKS_FILTER_SAVED_STATE_KEY\"\n\ndata class FilteringUiInfo(\n    val currentFilteringLabel: Int = R.string.label_all,\n    val noTasksLabel: Int = R.string.no_tasks_all,\n    val noTaskIconRes: Int = R.drawable.logo_no_fill,\n)\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/Async.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.util\n\n/**\n * A generic class that holds a loading signal or the result of an async operation.\n */\nsealed class Async<out T> {\n    object Loading : Async<Nothing>()\n\n    data class Error(val errorMessage: Int) : Async<Nothing>()\n\n    data class Success<out T>(val data: T) : Async<T>()\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/ComposeUtils.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.util\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport com.google.accompanist.swiperefresh.SwipeRefresh\nimport com.google.accompanist.swiperefresh.rememberSwipeRefreshState\n\nval primaryDarkColor: Color = Color(0xFF263238)\n\n/**\n * Display an initial empty state or swipe to refresh content.\n *\n * @param loading (state) when true, display a loading spinner over [content]\n * @param empty (state) when true, display [emptyContent]\n * @param emptyContent (slot) the content to display for the empty state\n * @param onRefresh (event) event to request refresh\n * @param modifier the modifier to apply to this layout.\n * @param content (slot) the main content to show\n */\n@Composable\nfun LoadingContent(\n    loading: Boolean,\n    empty: Boolean,\n    emptyContent: @Composable () -> Unit,\n    onRefresh: () -> Unit,\n    modifier: Modifier = Modifier,\n    content: @Composable () -> Unit\n) {\n    if (empty) {\n        emptyContent()\n    } else {\n        SwipeRefresh(\n            state = rememberSwipeRefreshState(loading),\n            onRefresh = onRefresh,\n            modifier = modifier,\n            content = content,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/CoroutinesUtils.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.util\n\nimport kotlinx.coroutines.flow.SharingStarted\n\nprivate const val StopTimeoutMillis: Long = 5000\n\n/**\n * A [SharingStarted] meant to be used with a [StateFlow] to expose data to the UI.\n *\n * When the UI stops observing, upstream flows stay active for some time to allow the system to\n * come back from a short-lived configuration change (such as rotations). If the UI stops\n * observing for longer, the cache is kept but the upstream flows are stopped. When the UI comes\n * back, the latest value is replayed and the upstream flows are executed again. This is done to\n * save resources when the app is in the background but let users switch between apps quickly.\n */\nval WhileUiSubscribed: SharingStarted = SharingStarted.WhileSubscribed(StopTimeoutMillis)\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/SimpleCountingIdlingResource.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.util\n\nimport androidx.test.espresso.IdlingResource\nimport java.util.concurrent.atomic.AtomicInteger\n\n/**\n * An simple counter implementation of [IdlingResource] that determines idleness by\n * maintaining an internal counter. When the counter is 0 - it is considered to be idle, when it is\n * non-zero it is not idle. This is very similar to the way a [java.util.concurrent.Semaphore]\n * behaves.\n *\n *\n * This class can then be used to wrap up operations that while in progress should block tests from\n * accessing the UI.\n */\nclass SimpleCountingIdlingResource(private val resourceName: String) : IdlingResource {\n\n    private val counter = AtomicInteger(0)\n\n    // written from main thread, read from any thread.\n    @Volatile\n    private var resourceCallback: IdlingResource.ResourceCallback? = null\n\n    override fun getName() = resourceName\n\n    override fun isIdleNow() = counter.get() == 0\n\n    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {\n        this.resourceCallback = resourceCallback\n    }\n\n    /**\n     * Increments the count of in-flight transactions to the resource being monitored.\n     */\n    fun increment() {\n        counter.getAndIncrement()\n    }\n\n    /**\n     * Decrements the count of in-flight transactions to the resource being monitored.\n     * If this operation results in the counter falling below 0 - an exception is raised.\n     *\n     * @throws IllegalStateException if the counter is below 0.\n     */\n    fun decrement() {\n        val counterVal = counter.decrementAndGet()\n        if (counterVal == 0) {\n            // we've gone from non-zero to zero. That means we're idle now! Tell espresso.\n            resourceCallback?.onTransitionToIdle()\n        } else if (counterVal < 0) {\n            throw IllegalStateException(\"Counter has been corrupted!\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TodoDrawer.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.util\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.DrawerState\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalNavigationDrawer\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinations\nimport com.example.android.architecture.blueprints.todoapp.TodoNavigationActions\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\n\n@Composable\nfun AppModalDrawer(\n    drawerState: DrawerState,\n    currentRoute: String,\n    navigationActions: TodoNavigationActions,\n    coroutineScope: CoroutineScope = rememberCoroutineScope(),\n    content: @Composable () -> Unit\n) {\n    ModalNavigationDrawer(\n        drawerState = drawerState,\n        drawerContent = {\n            AppDrawer(\n                currentRoute = currentRoute,\n                navigateToTasks = { navigationActions.navigateToTasks() },\n                navigateToStatistics = { navigationActions.navigateToStatistics() },\n                closeDrawer = { coroutineScope.launch { drawerState.close() } }\n            )\n        }\n    ) {\n        content()\n    }\n}\n\n@Composable\nprivate fun AppDrawer(\n    currentRoute: String,\n    navigateToTasks: () -> Unit,\n    navigateToStatistics: () -> Unit,\n    closeDrawer: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    Surface(color = MaterialTheme.colorScheme.background) {\n        Column(modifier = modifier.fillMaxSize()) {\n            DrawerHeader()\n            DrawerButton(\n                painter = painterResource(id = R.drawable.ic_list),\n                label = stringResource(id = R.string.list_title),\n                isSelected = currentRoute == TodoDestinations.TASKS_ROUTE,\n                action = {\n                    navigateToTasks()\n                    closeDrawer()\n                }\n            )\n            DrawerButton(\n                painter = painterResource(id = R.drawable.ic_statistics),\n                label = stringResource(id = R.string.statistics_title),\n                isSelected = currentRoute == TodoDestinations.STATISTICS_ROUTE,\n                action = {\n                    navigateToStatistics()\n                    closeDrawer()\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun DrawerHeader(\n    modifier: Modifier = Modifier\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n        modifier = modifier\n            .fillMaxWidth()\n            .background(primaryDarkColor)\n            .height(dimensionResource(id = R.dimen.header_height))\n            .padding(dimensionResource(id = R.dimen.header_padding))\n    ) {\n        Image(\n            painter = painterResource(id = R.drawable.logo_no_fill),\n            contentDescription =\n            stringResource(id = R.string.tasks_header_image_content_description),\n            modifier = Modifier.width(dimensionResource(id = R.dimen.header_image_width))\n        )\n        Text(\n            text = stringResource(id = R.string.navigation_view_header_title),\n            color = MaterialTheme.colorScheme.surface\n        )\n    }\n}\n\n@Composable\nprivate fun DrawerButton(\n    painter: Painter,\n    label: String,\n    isSelected: Boolean,\n    action: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    val tintColor = if (isSelected) {\n        MaterialTheme.colorScheme.secondary\n    } else {\n        MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n    }\n\n    TextButton(\n        onClick = action,\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(horizontal = dimensionResource(id = R.dimen.horizontal_margin))\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.Start,\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            Icon(\n                painter = painter,\n                contentDescription = null, // decorative\n                tint = tintColor\n            )\n            Spacer(Modifier.width(16.dp))\n            Text(\n                text = label,\n                style = MaterialTheme.typography.bodySmall,\n                color = tintColor\n            )\n        }\n    }\n}\n\n@Preview(\"Drawer contents\")\n@Composable\nfun PreviewAppDrawer() {\n    TodoTheme {\n        Surface {\n            AppDrawer(\n                currentRoute = TodoDestinations.TASKS_ROUTE,\n                navigateToTasks = {},\n                navigateToStatistics = {},\n                closeDrawer = {}\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/android/architecture/blueprints/todoapp/util/TopAppBars.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage com.example.android.architecture.blueprints.todoapp.util\n\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Menu\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoTheme\n\n@Composable\nfun TasksTopAppBar(\n    openDrawer: () -> Unit,\n    onFilterAllTasks: () -> Unit,\n    onFilterActiveTasks: () -> Unit,\n    onFilterCompletedTasks: () -> Unit,\n    onClearCompletedTasks: () -> Unit,\n    onRefresh: () -> Unit\n) {\n    TopAppBar(\n        title = { Text(text = stringResource(id = R.string.app_name)) },\n        navigationIcon = {\n            IconButton(onClick = openDrawer) {\n                Icon(Icons.Filled.Menu, stringResource(id = R.string.open_drawer))\n            }\n        },\n        actions = {\n            FilterTasksMenu(onFilterAllTasks, onFilterActiveTasks, onFilterCompletedTasks)\n            MoreTasksMenu(onClearCompletedTasks, onRefresh)\n        },\n        modifier = Modifier.fillMaxWidth()\n    )\n}\n\n@Composable\nprivate fun FilterTasksMenu(\n    onFilterAllTasks: () -> Unit,\n    onFilterActiveTasks: () -> Unit,\n    onFilterCompletedTasks: () -> Unit\n) {\n    TopAppBarDropdownMenu(\n        iconContent = {\n            Icon(\n                painterResource(id = R.drawable.ic_filter_list),\n                stringResource(id = R.string.menu_filter)\n            )\n        }\n    ) { closeMenu ->\n        DropdownMenuItem(onClick = { onFilterAllTasks(); closeMenu() },\n            text = { Text(text = stringResource(id = R.string.nav_all)) }\n        )\n        DropdownMenuItem(onClick = { onFilterActiveTasks(); closeMenu() },\n            text = { Text(text = stringResource(id = R.string.nav_active)) }\n        )\n        DropdownMenuItem(onClick = { onFilterCompletedTasks(); closeMenu() },\n            text = { Text(text = stringResource(id = R.string.nav_completed)) }\n        )\n    }\n}\n\n@Composable\nprivate fun MoreTasksMenu(\n    onClearCompletedTasks: () -> Unit,\n    onRefresh: () -> Unit\n) {\n    TopAppBarDropdownMenu(\n        iconContent = {\n            Icon(Icons.Filled.MoreVert, stringResource(id = R.string.menu_more))\n        }\n    ) { closeMenu ->\n        DropdownMenuItem(\n            text = { Text(text = stringResource(id = R.string.menu_clear)) },\n            onClick = { onClearCompletedTasks(); closeMenu() }\n        )\n        DropdownMenuItem(\n            text = { Text(text = stringResource(id = R.string.refresh)) },\n            onClick = { onRefresh(); closeMenu() }\n        )\n    }\n}\n\n@Composable\nprivate fun TopAppBarDropdownMenu(\n    iconContent: @Composable () -> Unit,\n    content: @Composable ColumnScope.(() -> Unit) -> Unit\n) {\n    var expanded by remember { mutableStateOf(false) }\n\n    Box(modifier = Modifier.wrapContentSize(Alignment.TopEnd)) {\n        IconButton(onClick = { expanded = !expanded }) {\n            iconContent()\n        }\n        DropdownMenu(\n            expanded = expanded,\n            onDismissRequest = { expanded = false },\n            modifier = Modifier.wrapContentSize(Alignment.TopEnd)\n        ) {\n            content { expanded = !expanded }\n        }\n    }\n}\n\n@Composable\nfun StatisticsTopAppBar(openDrawer: () -> Unit) {\n    TopAppBar(\n        title = { Text(text = stringResource(id = R.string.statistics_title)) },\n        navigationIcon = {\n            IconButton(onClick = openDrawer) {\n                Icon(Icons.Filled.Menu, stringResource(id = R.string.open_drawer))\n            }\n        },\n        modifier = Modifier.fillMaxWidth()\n    )\n}\n\n@Composable\nfun TaskDetailTopAppBar(onBack: () -> Unit, onDelete: () -> Unit) {\n    TopAppBar(\n        title = {\n            Text(text = stringResource(id = R.string.task_details))\n        },\n        navigationIcon = {\n            IconButton(onClick = onBack) {\n                Icon(Icons.Filled.ArrowBack, stringResource(id = R.string.menu_back))\n            }\n        },\n        actions = {\n            IconButton(onClick = onDelete) {\n                Icon(Icons.Filled.Delete, stringResource(id = R.string.menu_delete_task))\n            }\n        },\n        modifier = Modifier.fillMaxWidth()\n    )\n}\n\n@Composable\nfun AddEditTaskTopAppBar(@StringRes title: Int, onBack: () -> Unit) {\n    TopAppBar(\n        title = { Text(text = stringResource(title)) },\n        navigationIcon = {\n            IconButton(onClick = onBack) {\n                Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(id = R.string.menu_back))\n            }\n        },\n        modifier = Modifier.fillMaxWidth()\n    )\n}\n\n@Preview\n@Composable\nprivate fun TasksTopAppBarPreview() {\n    TodoTheme {\n        Surface {\n            TasksTopAppBar({}, {}, {}, {}, {}, {})\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun StatisticsTopAppBarPreview() {\n    TodoTheme {\n        Surface {\n            StatisticsTopAppBar { }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun TaskDetailTopAppBarPreview() {\n    TodoTheme {\n        Surface {\n            TaskDetailTopAppBar({ }, { })\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun AddEditTaskTopAppBarPreview() {\n    TodoTheme {\n        Surface {\n            AddEditTaskTopAppBar(R.string.add_task) { }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/drawer_item_color.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/colorAccent\" android:state_checked=\"true\" />\n    <item android:color=\"@color/colorGrey\" android:state_checked=\"false\" />\n</selector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_add.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_assignment_turned_in_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector android:alpha=\"0.49\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zm-7,0c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zm-2,14l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_check_circle_96dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:alpha=\"0.49\"\n    android:width=\"96dp\"\n    android:height=\"96dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"#2E7D32\"\n        android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm-2,15l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_done.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_filter_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#2E7D32\"\n        android:pathData=\"M10,18h4v-2h-4v2zM3,6v2h18V6H3zm3,7h12v-2H6v2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#2E7D32\"\n        android:pathData=\"M3,13h2v-2H3v2zm0,4h2v-2H3v2zm0,-8h2V7H3v2zm4,4h14v-2H7v2zm0,4h14v-2H7v2zM7,7v2h14V7H7z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M3,18h18v-2H3v2zm0,-5h18v-2H3v2zm0,-7v2h18V6H3z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_statistics.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_statistics_100dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector android:alpha=\"0.80\"\n    android:height=\"100dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"100dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_statistics_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM9,17H7v-7h2v7zm4,0h-2V7h2v10zm4,0h-2v-4h2v4z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_verified_user_96dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector\n    android:alpha=\"0.49\"\n    android:height=\"96dp\"\n    android:width=\"96dp\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#2E7D32\"\n        android:pathData=\"M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4zm-2,16l-4,-4 1.41,-1.41L10,14.17l6.59,-6.59L18,9l-8,8z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/list_completed_touch_feedback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:state_pressed=\"true\" android:drawable=\"@drawable/touchFeedback\" />\n\n    <item android:drawable=\"@drawable/completedTaskBackground\" />\n</selector>\n"
  },
  {
    "path": "app/src/main/res/drawable/touch_feedback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:state_pressed=\"true\" android:drawable=\"@drawable/touchFeedback\" />\n</selector>\n"
  },
  {
    "path": "app/src/main/res/font/opensans_font.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<font-family xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <font\n        app:fontStyle=\"normal\"\n        app:fontWeight=\"400\"\n        app:font=\"@font/opensans_regular\" />\n    <font\n        app:fontStyle=\"normal\"\n        app:fontWeight=\"700\"\n        app:font=\"@font/opensans_semibold\" />\n</font-family>\n"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<resources>\n    <declare-styleable name=\"ScrollChildSwipeRefreshLayout\">\n        <attr name=\"refreshing\" format=\"boolean\" />\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<resources>\n    <color name=\"colorPrimary\">#FFFFF0</color>\n    <color name=\"colorPrimaryDark\">#263238</color>\n    <color name=\"colorAccent\">#2E7D32</color>\n    <color name=\"colorTextPrimary\">#000000</color>\n    <color name=\"colorGrey\">#757575</color>\n\n    <drawable name=\"completedTaskBackground\">#CCCCCC</drawable>\n\n    <drawable name=\"touchFeedback\">#CFD8DC</drawable>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"horizontal_margin\">16dp</dimen>\n    <dimen name=\"vertical_margin\">16dp</dimen>\n\n    <dimen name=\"fab_margin\">16dp</dimen>\n\n    <dimen name=\"list_item_padding\">8dp</dimen>\n    <dimen name=\"header_height\">192dp</dimen>\n    <dimen name=\"header_padding\">16dp</dimen>\n    <dimen name=\"header_image_width\">100dp</dimen>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<resources>\n    <string name=\"app_name\">Todo</string>\n    <string name=\"add_task\">New Task</string>\n    <string name=\"edit_task\">Edit Task</string>\n    <string name=\"task_details\">Task Details</string>\n    <string name=\"task_marked_complete\">Task marked complete</string>\n    <string name=\"task_marked_active\">Task marked active</string>\n    <string name=\"loading_tasks_error\">Error while loading tasks</string>\n    <string name=\"loading_task_error\">Error while loading task</string>\n    <string name=\"task_not_found\">Task not found</string>\n    <string name=\"completed_tasks_cleared\">Completed tasks cleared</string>\n    <string name=\"menu_filter\">Filter</string>\n    <string name=\"open_drawer\">Open Drawer</string>\n    <string name=\"menu_back\">Back</string>\n    <string name=\"menu_clear\">Clear completed</string>\n    <string name=\"menu_delete_task\">Delete task</string>\n    <string name=\"menu_more\">More</string>\n    <string name=\"navigation_view_header_title\">Todo</string>\n    <string name=\"title_hint\">Title</string>\n    <string name=\"description_hint\">Enter your task here.</string>\n    <string name=\"cd_save_task\">Save task</string>\n    <string name=\"empty_task_message\">Tasks cannot be empty</string>\n    <string name=\"successfully_saved_task_message\">Task saved</string>\n    <string name=\"list_title\">Task List</string>\n    <string name=\"statistics_title\">Statistics</string>\n    <string name=\"statistics_no_tasks\">You have no tasks.</string>\n    <string name=\"statistics_active_tasks\">Active tasks: %.1f%%</string>\n    <string name=\"statistics_completed_tasks\">Completed tasks: %.1f%%</string>\n    <string name=\"statistics_error\">Error loading statistics.</string>\n    <string name=\"no_data\">No data</string>\n    <string name=\"loading\">LOADING</string>\n\n    <string-array name=\"list_tasks_array\">\n        <item>@string/nav_all</item>\n        <item>@string/nav_active</item>\n        <item>@string/nav_completed</item>\n    </string-array>\n    <string name=\"nav_all\">All</string>\n    <string name=\"nav_active\">Active</string>\n    <string name=\"nav_completed\">Completed</string>\n    <string name=\"label_all\">All Tasks</string>\n    <string name=\"label_active\">Active Tasks</string>\n    <string name=\"label_completed\">Completed Tasks</string>\n    <string name=\"no_tasks_all\">You have no tasks!</string>\n    <string name=\"no_tasks_active\">You have no active tasks!</string>\n    <string name=\"no_tasks_completed\">You have no completed tasks!</string>\n    <string name=\"refresh\">Refresh</string>\n    <string name=\"successfully_deleted_task_message\">Task was deleted</string>\n    <string name=\"successfully_added_task_message\">Task added</string>\n    <string name=\"no_data_description\" />\n\n    <!-- Content Descriptions -->\n    <string name=\"tasks_header_image_content_description\">Tasks header image</string>\n    <string name=\"no_tasks_image_content_description\">No tasks image</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<resources>\n\n    <!-- Base application theme. --><style name=\"AppTheme\" parent=\"Base.AppTheme\" />\n\n    <style name=\"AppTheme.OverlapSystemBar\" parent=\"Base.AppTheme\">\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">false</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n        <item name=\"android:windowTranslucentStatus\">true</item>\n        <item name=\"android:fitsSystemWindows\">true</item>\n    </style>\n\n    <style name=\"Base.AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n        <!-- For Android sdk versions 26+. -->\n        <item name=\"android:fontFamily\">@font/opensans_font</item>\n        <!-- Target Android sdk versions < 26 and > 14. -->\n        <item name=\"fontFamily\">@font/opensans_font</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-w820dp/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2019 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as screen margins) for screens with more than 820dp of available width. This\n         would include 7\" and 10\" devices in landscape (~960dp and ~1280dp respectively). -->\n    <dimen name=\"horizontal_margin\">64dp</dimen>\n\n    <dimen name=\"fab_margin\">24dp</dimen>\n\n</resources>\n"
  },
  {
    "path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskViewModelTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.addedittask\n\nimport androidx.lifecycle.SavedStateHandle\nimport com.example.android.architecture.blueprints.todoapp.MainCoroutineRule\nimport com.example.android.architecture.blueprints.todoapp.R.string\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs\nimport com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.google.common.truth.Truth.assertThat\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.advanceUntilIdle\nimport kotlinx.coroutines.test.runTest\nimport kotlinx.coroutines.test.setMain\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\n\n/**\n * Unit tests for the implementation of [AddEditTaskViewModel].\n */\n@ExperimentalCoroutinesApi\nclass AddEditTaskViewModelTest {\n\n    // Subject under test\n    private lateinit var addEditTaskViewModel: AddEditTaskViewModel\n\n    // Use a fake repository to be injected into the viewmodel\n    private lateinit var tasksRepository: FakeTaskRepository\n    private val task = Task(title = \"Title1\", description = \"Description1\", id = \"0\")\n\n    // Set the main coroutines dispatcher for unit testing.\n    @ExperimentalCoroutinesApi\n    @get:Rule\n    val mainCoroutineRule = MainCoroutineRule()\n\n    @Before\n    fun setupViewModel() {\n        // We initialise the repository with no tasks\n        tasksRepository = FakeTaskRepository().apply {\n            addTasks(task)\n        }\n    }\n\n    @Test\n    fun saveNewTaskToRepository_showsSuccessMessageUi() {\n        addEditTaskViewModel = AddEditTaskViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"0\"))\n        )\n\n        val newTitle = \"New Task Title\"\n        val newDescription = \"Some Task Description\"\n        addEditTaskViewModel.apply {\n            updateTitle(newTitle)\n            updateDescription(newDescription)\n        }\n        addEditTaskViewModel.saveTask()\n\n        val newTask = tasksRepository.savedTasks.value.values.first()\n\n        // Then a task is saved in the repository and the view updated\n        assertThat(newTask.title).isEqualTo(newTitle)\n        assertThat(newTask.description).isEqualTo(newDescription)\n    }\n\n    @Test\n    fun loadTasks_loading() = runTest {\n        // Set Main dispatcher to not run coroutines eagerly, for just this one test\n        Dispatchers.setMain(StandardTestDispatcher())\n\n        addEditTaskViewModel = AddEditTaskViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"0\"))\n        )\n\n        // Then progress indicator is shown\n        assertThat(addEditTaskViewModel.uiState.value.isLoading).isTrue()\n\n        // Execute pending coroutines actions\n        advanceUntilIdle()\n\n        // Then progress indicator is hidden\n        assertThat(addEditTaskViewModel.uiState.value.isLoading).isFalse()\n    }\n\n    @Test\n    fun loadTasks_taskShown() {\n        addEditTaskViewModel = AddEditTaskViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"0\"))\n        )\n\n        // Add task to repository\n        tasksRepository.addTasks(task)\n\n        // Verify a task is loaded\n        val uiState = addEditTaskViewModel.uiState.value\n        assertThat(uiState.title).isEqualTo(task.title)\n        assertThat(uiState.description).isEqualTo(task.description)\n        assertThat(uiState.isLoading).isFalse()\n    }\n\n    @Test\n    fun saveNewTaskToRepository_emptyTitle_error() {\n        addEditTaskViewModel = AddEditTaskViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"0\"))\n        )\n\n        saveTaskAndAssertUserMessage(\"\", \"Some Task Description\")\n    }\n\n    @Test\n    fun saveNewTaskToRepository_emptyDescription_error() {\n        addEditTaskViewModel = AddEditTaskViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"0\"))\n        )\n\n        saveTaskAndAssertUserMessage(\"Title\", \"\")\n    }\n\n    @Test\n    fun saveNewTaskToRepository_emptyDescriptionEmptyTitle_error() {\n        addEditTaskViewModel = AddEditTaskViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"0\"))\n        )\n\n        saveTaskAndAssertUserMessage(\"\", \"\")\n    }\n\n    private fun saveTaskAndAssertUserMessage(title: String, description: String) {\n        addEditTaskViewModel.apply {\n            updateTitle(title)\n            updateDescription(description)\n        }\n\n        // When saving an incomplete task\n        addEditTaskViewModel.saveTask()\n\n        assertThat(\n            addEditTaskViewModel.uiState.value.userMessage\n        ).isEqualTo(string.empty_task_message)\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/data/DefaultTaskRepositoryTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data\n\nimport com.example.android.architecture.blueprints.todoapp.data.source.local.FakeTaskDao\nimport com.example.android.architecture.blueprints.todoapp.data.source.network.FakeNetworkDataSource\nimport com.google.common.truth.Truth.assertThat\nimport junit.framework.TestCase.assertEquals\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.TestScope\nimport kotlinx.coroutines.test.UnconfinedTestDispatcher\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Test\n\n/**\n * Unit tests for the implementation of the in-memory repository with cache.\n */\n@ExperimentalCoroutinesApi\nclass DefaultTaskRepositoryTest {\n\n    private val task1 = Task(id = \"1\", title = \"Title1\", description = \"Description1\")\n    private val task2 = Task(id = \"2\", title = \"Title2\", description = \"Description2\")\n    private val task3 = Task(id = \"3\", title = \"Title3\", description = \"Description3\")\n\n    private val newTaskTitle = \"Title new\"\n    private val newTaskDescription = \"Description new\"\n    private val newTask = Task(id = \"new\", title = newTaskTitle, description = newTaskDescription)\n    private val newTasks = listOf(newTask)\n\n    private val networkTasks = listOf(task1, task2).toNetwork()\n    private val localTasks = listOf(task3.toLocal())\n\n    // Test dependencies\n    private lateinit var networkDataSource: FakeNetworkDataSource\n    private lateinit var localDataSource: FakeTaskDao\n\n    private var testDispatcher = UnconfinedTestDispatcher()\n    private var testScope = TestScope(testDispatcher)\n\n    // Class under test\n    private lateinit var taskRepository: DefaultTaskRepository\n\n    @ExperimentalCoroutinesApi\n    @Before\n    fun createRepository() {\n        networkDataSource = FakeNetworkDataSource(networkTasks.toMutableList())\n        localDataSource = FakeTaskDao(localTasks)\n        // Get a reference to the class under test\n        taskRepository = DefaultTaskRepository(\n            networkDataSource = networkDataSource,\n            localDataSource = localDataSource,\n            dispatcher = testDispatcher,\n            scope = testScope\n        )\n    }\n\n    @ExperimentalCoroutinesApi\n    @Test\n    fun getTasks_emptyRepositoryAndUninitializedCache() = testScope.runTest {\n        networkDataSource.tasks?.clear()\n        localDataSource.deleteAll()\n\n        assertThat(taskRepository.getTasks().size).isEqualTo(0)\n    }\n\n    @Test\n    fun getTasks_repositoryCachesAfterFirstApiCall() = testScope.runTest {\n        // Trigger the repository to load tasks from the remote data source\n        val initial = taskRepository.getTasks(forceUpdate = true)\n\n        // Change the remote data source\n        networkDataSource.tasks = newTasks.toNetwork().toMutableList()\n\n        // Load the tasks again without forcing a refresh\n        val second = taskRepository.getTasks()\n\n        // Initial and second should match because we didn't force a refresh (no tasks were loaded\n        // from the remote data source)\n        assertThat(second).isEqualTo(initial)\n    }\n\n    @Test\n    fun getTasks_requestsAllTasksFromRemoteDataSource() = testScope.runTest {\n        // When tasks are requested from the tasks repository\n        val tasks = taskRepository.getTasks(true)\n\n        // Then tasks are loaded from the remote data source\n        assertThat(tasks).isEqualTo(networkTasks.toExternal())\n    }\n\n    @Test\n    fun saveTask_savesToLocalAndRemote() = testScope.runTest {\n        // When a task is saved to the tasks repository\n        val newTaskId = taskRepository.createTask(newTask.title, newTask.description)\n\n        // Then the remote and local sources contain the new task\n        assertThat(networkDataSource.tasks?.map { it.id }?.contains(newTaskId))\n        assertThat(localDataSource.tasks?.map { it.id }?.contains(newTaskId))\n    }\n\n    @Test\n    fun getTasks_WithDirtyCache_tasksAreRetrievedFromRemote() = testScope.runTest {\n        // First call returns from REMOTE\n        val tasks = taskRepository.getTasks()\n\n        // Set a different list of tasks in REMOTE\n        networkDataSource.tasks = newTasks.toNetwork().toMutableList()\n\n        // But if tasks are cached, subsequent calls load from cache\n        val cachedTasks = taskRepository.getTasks()\n        assertThat(cachedTasks).isEqualTo(tasks)\n\n        // Now force remote loading\n        val refreshedTasks = taskRepository.getTasks(true)\n\n        // Tasks must be the recently updated in REMOTE\n        assertThat(refreshedTasks).isEqualTo(newTasks)\n    }\n\n    @Test(expected = Exception::class)\n    fun getTasks_WithDirtyCache_remoteUnavailable_throwsException() = testScope.runTest {\n        // Make remote data source unavailable\n        networkDataSource.tasks = null\n\n        // Load tasks forcing remote load\n        taskRepository.getTasks(true)\n\n        // Exception should be thrown\n    }\n\n    @Test\n    fun getTasks_WithRemoteDataSourceUnavailable_tasksAreRetrievedFromLocal() =\n        testScope.runTest {\n            // When the remote data source is unavailable\n            networkDataSource.tasks = null\n\n            // The repository fetches from the local source\n            assertThat(taskRepository.getTasks()).isEqualTo(localTasks.toExternal())\n        }\n\n    @Test(expected = Exception::class)\n    fun getTasks_WithBothDataSourcesUnavailable_throwsError() = testScope.runTest {\n        // When both sources are unavailable\n        networkDataSource.tasks = null\n        localDataSource.tasks = null\n\n        // The repository throws an error\n        taskRepository.getTasks()\n    }\n\n    @Test\n    fun getTasks_refreshesLocalDataSource() = testScope.runTest {\n        // Forcing an update will fetch tasks from remote\n        val expectedTasks = networkTasks.toExternal()\n\n        val newTasks = taskRepository.getTasks(true)\n\n        assertEquals(expectedTasks, newTasks)\n        assertEquals(expectedTasks, localDataSource.tasks?.toExternal())\n    }\n\n    @Test\n    fun completeTask_completesTaskToServiceAPIUpdatesCache() = testScope.runTest {\n        // Save a task\n        val newTaskId = taskRepository.createTask(newTask.title, newTask.description)\n\n        // Make sure it's active\n        assertThat(taskRepository.getTask(newTaskId)?.isCompleted).isFalse()\n\n        // Mark is as complete\n        taskRepository.completeTask(newTaskId)\n\n        // Verify it's now completed\n        assertThat(taskRepository.getTask(newTaskId)?.isCompleted).isTrue()\n    }\n\n    @Test\n    fun completeTask_activeTaskToServiceAPIUpdatesCache() = testScope.runTest {\n        // Save a task\n        val newTaskId = taskRepository.createTask(newTask.title, newTask.description)\n        taskRepository.completeTask(newTaskId)\n\n        // Make sure it's completed\n        assertThat(taskRepository.getTask(newTaskId)?.isActive).isFalse()\n\n        // Mark is as active\n        taskRepository.activateTask(newTaskId)\n\n        // Verify it's now activated\n        assertThat(taskRepository.getTask(newTaskId)?.isActive).isTrue()\n    }\n\n    @Test\n    fun getTask_repositoryCachesAfterFirstApiCall() = testScope.runTest {\n        // Obtain a task from the local data source\n        localDataSource = FakeTaskDao(mutableListOf(task1.toLocal()))\n        val initial = taskRepository.getTask(task1.id)\n\n        // Change the tasks on the remote\n        networkDataSource.tasks = newTasks.toNetwork().toMutableList()\n\n        // Obtain the same task again\n        val second = taskRepository.getTask(task1.id)\n\n        // Initial and second tasks should match because we didn't force a refresh\n        assertThat(second).isEqualTo(initial)\n    }\n\n    @Test\n    fun getTask_forceRefresh() = testScope.runTest {\n        // Trigger the repository to load data, which loads from remote and caches\n        networkDataSource.tasks = mutableListOf(task1.toNetwork())\n        val task1FirstTime = taskRepository.getTask(task1.id, forceUpdate = true)\n        assertThat(task1FirstTime?.id).isEqualTo(task1.id)\n\n        // Configure the remote data source to return a different task\n        networkDataSource.tasks = mutableListOf(task2.toNetwork())\n\n        // Force refresh\n        val task1SecondTime = taskRepository.getTask(task1.id, true)\n        val task2SecondTime = taskRepository.getTask(task2.id, true)\n\n        // Only task2 works because task1 does not exist on the remote\n        assertThat(task1SecondTime).isNull()\n        assertThat(task2SecondTime?.id).isEqualTo(task2.id)\n    }\n\n    @Test\n    fun clearCompletedTasks() = testScope.runTest {\n        val completedTask = task1.copy(isCompleted = true)\n        localDataSource.tasks = listOf(completedTask.toLocal(), task2.toLocal())\n        taskRepository.clearCompletedTasks()\n\n        val tasks = taskRepository.getTasks(true)\n\n        assertThat(tasks).hasSize(1)\n        assertThat(tasks).contains(task2)\n        assertThat(tasks).doesNotContain(completedTask)\n    }\n\n    @Test\n    fun deleteAllTasks() = testScope.runTest {\n        val initialTasks = taskRepository.getTasks()\n\n        // Verify tasks are returned\n        assertThat(initialTasks.size).isEqualTo(1)\n\n        // Delete all tasks\n        taskRepository.deleteAllTasks()\n\n        // Verify tasks are empty now\n        val afterDeleteTasks = taskRepository.getTasks()\n        assertThat(afterDeleteTasks).isEmpty()\n    }\n\n    @Test\n    fun deleteSingleTask() = testScope.runTest {\n        val initialTasksSize = taskRepository.getTasks(true).size\n\n        // Delete first task\n        taskRepository.deleteTask(task1.id)\n\n        // Fetch data again\n        val afterDeleteTasks = taskRepository.getTasks(true)\n\n        // Verify only one task was deleted\n        assertThat(afterDeleteTasks.size).isEqualTo(initialTasksSize - 1)\n        assertThat(afterDeleteTasks).doesNotContain(task1)\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsUtilsTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.statistics\n\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport org.hamcrest.core.Is.`is`\nimport org.junit.Assert.assertThat\nimport org.junit.Test\n\n/**\n * Unit tests for [getActiveAndCompletedStats].\n */\nclass StatisticsUtilsTest {\n\n    @Test\n    fun getActiveAndCompletedStats_noCompleted() {\n        val tasks = listOf(\n            Task(\n                id = \"id\",\n                title = \"title\",\n                description = \"desc\",\n                isCompleted = false,\n            )\n        )\n        // When the list of tasks is computed with an active task\n        val result = getActiveAndCompletedStats(tasks)\n\n        // Then the percentages are 100 and 0\n        assertThat(result.activeTasksPercent, `is`(100f))\n        assertThat(result.completedTasksPercent, `is`(0f))\n    }\n\n    @Test\n    fun getActiveAndCompletedStats_noActive() {\n        val tasks = listOf(\n            Task(\n                id = \"id\",\n                title = \"title\",\n                description = \"desc\",\n                isCompleted = true,\n            )\n        )\n        // When the list of tasks is computed with a completed task\n        val result = getActiveAndCompletedStats(tasks)\n\n        // Then the percentages are 0 and 100\n        assertThat(result.activeTasksPercent, `is`(0f))\n        assertThat(result.completedTasksPercent, `is`(100f))\n    }\n\n    @Test\n    fun getActiveAndCompletedStats_both() {\n        // Given 3 completed tasks and 2 active tasks\n        val tasks = listOf(\n            Task(id = \"1\", title = \"title\", description = \"desc\", isCompleted = true),\n            Task(id = \"2\", title = \"title\", description = \"desc\", isCompleted = true),\n            Task(id = \"3\", title = \"title\", description = \"desc\", isCompleted = true),\n            Task(id = \"4\", title = \"title\", description = \"desc\", isCompleted = false),\n            Task(id = \"5\", title = \"title\", description = \"desc\", isCompleted = false),\n        )\n        // When the list of tasks is computed\n        val result = getActiveAndCompletedStats(tasks)\n\n        // Then the result is 40-60\n        assertThat(result.activeTasksPercent, `is`(40f))\n        assertThat(result.completedTasksPercent, `is`(60f))\n    }\n\n    @Test\n    fun getActiveAndCompletedStats_empty() {\n        // When there are no tasks\n        val result = getActiveAndCompletedStats(emptyList())\n\n        // Both active and completed tasks are 0\n        assertThat(result.activeTasksPercent, `is`(0f))\n        assertThat(result.completedTasksPercent, `is`(0f))\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/statistics/StatisticsViewModelTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.statistics\n\nimport com.example.android.architecture.blueprints.todoapp.MainCoroutineRule\nimport com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.google.common.truth.Truth.assertThat\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.advanceUntilIdle\nimport kotlinx.coroutines.test.runTest\nimport kotlinx.coroutines.test.setMain\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\n\n/**\n * Unit tests for the implementation of [StatisticsViewModel]\n */\n@ExperimentalCoroutinesApi\nclass StatisticsViewModelTest {\n\n    // Subject under test\n    private lateinit var statisticsViewModel: StatisticsViewModel\n\n    // Use a fake repository to be injected into the viewmodel\n    private lateinit var tasksRepository: FakeTaskRepository\n\n    // Set the main coroutines dispatcher for unit testing.\n    @ExperimentalCoroutinesApi\n    @get:Rule\n    val mainCoroutineRule = MainCoroutineRule()\n\n    @Before\n    fun setupStatisticsViewModel() {\n        tasksRepository = FakeTaskRepository()\n        statisticsViewModel = StatisticsViewModel(tasksRepository)\n    }\n\n    @Test\n    fun loadEmptyTasksFromRepository_EmptyResults() = runTest {\n        // Given an initialized StatisticsViewModel with no tasks\n\n        // Then the results are empty\n        val uiState = statisticsViewModel.uiState.first()\n        assertThat(uiState.isEmpty).isTrue()\n    }\n\n    @Test\n    fun loadNonEmptyTasksFromRepository_NonEmptyResults() = runTest {\n        // We initialise the tasks to 3, with one active and two completed\n        val task1 = Task(id = \"1\", title = \"Title1\", description = \"Desc1\")\n        val task2 = Task(id = \"2\", title = \"Title2\", description = \"Desc2\", isCompleted = true)\n        val task3 = Task(id = \"3\", title = \"Title3\", description = \"Desc3\", isCompleted = true)\n        val task4 = Task(id = \"4\", title = \"Title4\", description = \"Desc4\", isCompleted = true)\n        tasksRepository.addTasks(task1, task2, task3, task4)\n\n        // Then the results are not empty\n        val uiState = statisticsViewModel.uiState.first()\n        assertThat(uiState.isEmpty).isFalse()\n        assertThat(uiState.activeTasksPercent).isEqualTo(25f)\n        assertThat(uiState.completedTasksPercent).isEqualTo(75f)\n        assertThat(uiState.isLoading).isEqualTo(false)\n    }\n\n    @Test\n    fun loadTasks_loading() = runTest {\n        // Set Main dispatcher to not run coroutines eagerly, for just this one test\n        Dispatchers.setMain(StandardTestDispatcher())\n\n        var isLoading: Boolean? = true\n        val job = launch {\n            statisticsViewModel.uiState.collect {\n                isLoading = it.isLoading\n            }\n        }\n\n        // Then progress indicator is shown\n        assertThat(isLoading).isTrue()\n\n        // Execute pending coroutines actions\n        advanceUntilIdle()\n\n        // Then progress indicator is hidden\n        assertThat(isLoading).isFalse()\n        job.cancel()\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/taskdetail/TaskDetailViewModelTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.taskdetail\n\nimport androidx.lifecycle.SavedStateHandle\nimport com.example.android.architecture.blueprints.todoapp.MainCoroutineRule\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs\nimport com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.google.common.truth.Truth.assertThat\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.advanceUntilIdle\nimport kotlinx.coroutines.test.runTest\nimport kotlinx.coroutines.test.setMain\nimport org.junit.Assert.assertTrue\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\n\n/**\n * Unit tests for the implementation of [TaskDetailViewModel]\n */\n@ExperimentalCoroutinesApi\nclass TaskDetailViewModelTest {\n\n    // Set the main coroutines dispatcher for unit testing.\n    @ExperimentalCoroutinesApi\n    @get:Rule\n    val mainCoroutineRule = MainCoroutineRule()\n\n    // Subject under test\n    private lateinit var taskDetailViewModel: TaskDetailViewModel\n\n    // Use a fake repository to be injected into the viewmodel\n    private lateinit var tasksRepository: FakeTaskRepository\n    private val task = Task(title = \"Title1\", description = \"Description1\", id = \"0\")\n\n    @Before\n    fun setupViewModel() {\n        tasksRepository = FakeTaskRepository()\n        tasksRepository.addTasks(task)\n\n        taskDetailViewModel = TaskDetailViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"0\"))\n        )\n    }\n\n    @Test\n    fun getActiveTaskFromRepositoryAndLoadIntoView() = runTest {\n        val uiState = taskDetailViewModel.uiState.first()\n        // Then verify that the view was notified\n        assertThat(uiState.task?.title).isEqualTo(task.title)\n        assertThat(uiState.task?.description).isEqualTo(task.description)\n    }\n\n    @Test\n    fun completeTask() = runTest {\n        // Verify that the task was active initially\n        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isFalse()\n\n        // When the ViewModel is asked to complete the task\n        assertThat(taskDetailViewModel.uiState.first().task?.id).isEqualTo(\"0\")\n        taskDetailViewModel.setCompleted(true)\n\n        // Then the task is completed and the snackbar shows the correct message\n        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isTrue()\n        assertThat(taskDetailViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.task_marked_complete)\n    }\n\n    @Test\n    fun activateTask() = runTest {\n        tasksRepository.deleteAllTasks()\n        tasksRepository.addTasks(task.copy(isCompleted = true))\n\n        // Verify that the task was completed initially\n        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isTrue()\n\n        // When the ViewModel is asked to complete the task\n        assertThat(taskDetailViewModel.uiState.first().task?.id).isEqualTo(\"0\")\n        taskDetailViewModel.setCompleted(false)\n\n        // Then the task is not completed and the snackbar shows the correct message\n        val newTask = tasksRepository.getTask(task.id)\n        assertTrue((newTask?.isActive) ?: false)\n        assertThat(taskDetailViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.task_marked_active)\n    }\n\n    @Test\n    fun taskDetailViewModel_repositoryError() = runTest {\n        // Given a repository that throws errors\n        tasksRepository.setShouldThrowError(true)\n\n        // Then the task is null and the snackbar shows a loading error message\n        assertThat(taskDetailViewModel.uiState.value.task).isNull()\n        assertThat(taskDetailViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.loading_task_error)\n    }\n\n    @Test\n    fun taskDetailViewModel_taskNotFound() = runTest {\n        // Given an ID for a non existent task\n        taskDetailViewModel = TaskDetailViewModel(\n            tasksRepository,\n            SavedStateHandle(mapOf(TodoDestinationsArgs.TASK_ID_ARG to \"nonexistent_id\"))\n        )\n\n        // The task is null and the snackbar shows a \"not found\" error message\n        assertThat(taskDetailViewModel.uiState.value.task).isNull()\n        assertThat(taskDetailViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.task_not_found)\n    }\n\n    @Test\n    fun deleteTask() = runTest {\n        assertThat(tasksRepository.savedTasks.value.containsValue(task)).isTrue()\n\n        // When the deletion of a task is requested\n        taskDetailViewModel.deleteTask()\n\n        assertThat(tasksRepository.savedTasks.value.containsValue(task)).isFalse()\n    }\n\n    @Test\n    fun loadTask_loading() = runTest {\n        // Set Main dispatcher to not run coroutines eagerly, for just this one test\n        Dispatchers.setMain(StandardTestDispatcher())\n\n        var isLoading: Boolean? = true\n        val job = launch {\n            taskDetailViewModel.uiState.collect {\n                isLoading = it.isLoading\n            }\n        }\n\n        // Then progress indicator is shown\n        assertThat(isLoading).isTrue()\n\n        // Execute pending coroutines actions\n        advanceUntilIdle()\n\n        // Then progress indicator is hidden\n        assertThat(isLoading).isFalse()\n        job.cancel()\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksViewModelTest.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.tasks\n\nimport androidx.lifecycle.SavedStateHandle\nimport com.example.android.architecture.blueprints.todoapp.ADD_EDIT_RESULT_OK\nimport com.example.android.architecture.blueprints.todoapp.DELETE_RESULT_OK\nimport com.example.android.architecture.blueprints.todoapp.EDIT_RESULT_OK\nimport com.example.android.architecture.blueprints.todoapp.MainCoroutineRule\nimport com.example.android.architecture.blueprints.todoapp.R\nimport com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository\nimport com.example.android.architecture.blueprints.todoapp.data.Task\nimport com.google.common.truth.Truth.assertThat\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.test.StandardTestDispatcher\nimport kotlinx.coroutines.test.advanceUntilIdle\nimport kotlinx.coroutines.test.runTest\nimport kotlinx.coroutines.test.setMain\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\n\n/**\n * Unit tests for the implementation of [TasksViewModel]\n */\n@ExperimentalCoroutinesApi\nclass TasksViewModelTest {\n\n    // Subject under test\n    private lateinit var tasksViewModel: TasksViewModel\n\n    // Use a fake repository to be injected into the viewmodel\n    private lateinit var tasksRepository: FakeTaskRepository\n\n    // Set the main coroutines dispatcher for unit testing.\n    @ExperimentalCoroutinesApi\n    @get:Rule\n    val mainCoroutineRule = MainCoroutineRule()\n\n    @Before\n    fun setupViewModel() {\n        // We initialise the tasks to 3, with one active and two completed\n        tasksRepository = FakeTaskRepository()\n        val task1 = Task(id = \"1\", title = \"Title1\", description = \"Desc1\")\n        val task2 = Task(id = \"2\", title = \"Title2\", description = \"Desc2\", isCompleted = true)\n        val task3 = Task(id = \"3\", title = \"Title3\", description = \"Desc3\", isCompleted = true)\n        tasksRepository.addTasks(task1, task2, task3)\n\n        tasksViewModel = TasksViewModel(tasksRepository, SavedStateHandle())\n    }\n\n    @Test\n    fun loadAllTasksFromRepository_loadingTogglesAndDataLoaded() = runTest {\n        // Set Main dispatcher to not run coroutines eagerly, for just this one test\n        Dispatchers.setMain(StandardTestDispatcher())\n\n        // Given an initialized TasksViewModel with initialized tasks\n        // When loading of Tasks is requested\n        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)\n\n        // Trigger loading of tasks\n        tasksViewModel.refresh()\n\n        // Then progress indicator is shown\n        assertThat(tasksViewModel.uiState.first().isLoading).isTrue()\n\n        // Execute pending coroutines actions\n        advanceUntilIdle()\n\n        // Then progress indicator is hidden\n        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()\n\n        // And data correctly loaded\n        assertThat(tasksViewModel.uiState.first().items).hasSize(3)\n    }\n\n    @Test\n    fun loadActiveTasksFromRepositoryAndLoadIntoView() = runTest {\n        // Given an initialized TasksViewModel with initialized tasks\n        // When loading of Tasks is requested\n        tasksViewModel.setFiltering(TasksFilterType.ACTIVE_TASKS)\n\n        // Load tasks\n        tasksViewModel.refresh()\n\n        // Then progress indicator is hidden\n        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()\n\n        // And data correctly loaded\n        assertThat(tasksViewModel.uiState.first().items).hasSize(1)\n    }\n\n    @Test\n    fun loadCompletedTasksFromRepositoryAndLoadIntoView() = runTest {\n        // Given an initialized TasksViewModel with initialized tasks\n        // When loading of Tasks is requested\n        tasksViewModel.setFiltering(TasksFilterType.COMPLETED_TASKS)\n\n        // Load tasks\n        tasksViewModel.refresh()\n\n        // Then progress indicator is hidden\n        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()\n\n        // And data correctly loaded\n        assertThat(tasksViewModel.uiState.first().items).hasSize(2)\n    }\n\n    @Test\n    fun loadTasks_error() = runTest {\n        // Make the repository throw errors\n        tasksRepository.setShouldThrowError(true)\n\n        // Load tasks\n        tasksViewModel.refresh()\n\n        // Then progress indicator is hidden\n        assertThat(tasksViewModel.uiState.first().isLoading).isFalse()\n\n        // And the list of items is empty\n        assertThat(tasksViewModel.uiState.first().items).isEmpty()\n        assertThat(tasksViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.loading_tasks_error)\n    }\n\n    @Test\n    fun clearCompletedTasks_clearsTasks() = runTest {\n        // When completed tasks are cleared\n        tasksViewModel.clearCompletedTasks()\n\n        // Fetch tasks\n        tasksViewModel.refresh()\n\n        // Fetch tasks\n        val allTasks = tasksViewModel.uiState.first().items\n        val completedTasks = allTasks?.filter { it.isCompleted }\n\n        // Verify there are no completed tasks left\n        assertThat(completedTasks).isEmpty()\n\n        // Verify active task is not cleared\n        assertThat(allTasks).hasSize(1)\n\n        // Verify snackbar is updated\n        assertThat(tasksViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.completed_tasks_cleared)\n    }\n\n    @Test\n    fun showEditResultMessages_editOk_snackbarUpdated() = runTest {\n        // When the viewmodel receives a result from another destination\n        tasksViewModel.showEditResultMessage(EDIT_RESULT_OK)\n\n        // The snackbar is updated\n        assertThat(tasksViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.successfully_saved_task_message)\n    }\n\n    @Test\n    fun showEditResultMessages_addOk_snackbarUpdated() = runTest {\n        // When the viewmodel receives a result from another destination\n        tasksViewModel.showEditResultMessage(ADD_EDIT_RESULT_OK)\n\n        // The snackbar is updated\n        assertThat(tasksViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.successfully_added_task_message)\n    }\n\n    @Test\n    fun showEditResultMessages_deleteOk_snackbarUpdated() = runTest {\n        // When the viewmodel receives a result from another destination\n        tasksViewModel.showEditResultMessage(DELETE_RESULT_OK)\n\n        // The snackbar is updated\n        assertThat(tasksViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.successfully_deleted_task_message)\n    }\n\n    @Test\n    fun completeTask_dataAndSnackbarUpdated() = runTest {\n        // With a repository that has an active task\n        val task = Task(id = \"id\", title = \"Title\", description = \"Description\")\n        tasksRepository.addTasks(task)\n\n        // Complete task\n        tasksViewModel.completeTask(task, true)\n\n        // Verify the task is completed\n        assertThat(tasksRepository.savedTasks.value[task.id]?.isCompleted).isTrue()\n\n        // The snackbar is updated\n        assertThat(tasksViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.task_marked_complete)\n    }\n\n    @Test\n    fun activateTask_dataAndSnackbarUpdated() = runTest {\n        // With a repository that has a completed task\n        val task = Task(id = \"id\", title = \"Title\", description = \"Description\", isCompleted = true)\n        tasksRepository.addTasks(task)\n\n        // Activate task\n        tasksViewModel.completeTask(task, false)\n\n        // Verify the task is active\n        assertThat(tasksRepository.savedTasks.value[task.id]?.isActive).isTrue()\n\n        // The snackbar is updated\n        assertThat(tasksViewModel.uiState.first().userMessage)\n            .isEqualTo(R.string.task_marked_active)\n    }\n}\n"
  },
  {
    "path": "app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker",
    "content": "mock-maker-inline\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nplugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.android.library) apply false\n    alias(libs.plugins.kotlin.android) apply false\n    alias(libs.plugins.ksp) apply false\n    alias(libs.plugins.hilt) apply false\n    alias(libs.plugins.compose.compiler) apply false\n}\n"
  },
  {
    "path": "gradle/init.gradle.kts",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n *   Licensed under the Apache License, Version 2.0 (the \"License\");\n *   you may not use this file except in compliance with the License.\n *   You may obtain a copy of the License at\n *\n *       https://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software\n *   distributed under the License is distributed on an \"AS IS\" BASIS,\n *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *   See the License for the specific language governing permissions and\n *   limitations under the License.\n */\n\n// The init script is used to run Spotless in a gradle configuration cache compliant manner as\n// Spotless itself is not gradle configuration cache compliant.\n// Note that the init script needs to be run with the configuration cache turned off.\n\nval ktlintVersion = \"0.44.0\"\n\ninitscript {\n    val spotlessVersion = \"6.25.0\"\n\n    repositories {\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath(\"com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion\")\n    }\n}\n\nrootProject {\n    subprojects {\n        apply<com.diffplug.gradle.spotless.SpotlessPlugin>()\n        extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {\n            kotlin {\n                target(\"**/*.kt\")\n                targetExclude(\"**/build/**/*.kt\")\n                ktlint(ktlintVersion).userData(mapOf(\"android\" to \"true\"))\n                licenseHeaderFile(rootProject.file(\"spotless/copyright.kt\"))\n            }\n            format(\"kts\") {\n                target(\"**/*.kts\")\n                targetExclude(\"**/build/**/*.kts\")\n                // Look for the first line that doesn't have a block comment (assumed to be the license)\n                licenseHeaderFile(rootProject.file(\"spotless/copyright.kts\"), \"(^(?![\\\\/ ]\\\\*).*$)\")\n            }\n            format(\"xml\") {\n                target(\"**/*.xml\")\n                targetExclude(\"**/build/**/*.xml\")\n                // Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)\n                licenseHeaderFile(rootProject.file(\"spotless/copyright.xml\"), \"(<[^!?])\")\n            }\n        }\n    }\n}"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\naccompanist = \"0.36.0\"\nannotation = \"1.9.1\"\nandroidDesugarJdkLibs = \"2.1.3\"\nandroidGradlePlugin = \"8.7.3\"\nandroidxActivity = \"1.9.3\"\nandroidxAppCompat = \"1.7.0\"\nandroidxArchCore = \"2.2.0\"\nandroidxBrowser = \"1.4.0\"\nandroidxComposeBom = \"2024.12.01\"\nandroidxCompose = \"1.2.0\"\nandroidxComposeRuntimeTracing = \"1.7.6\"\nandroidxCore = \"1.15.0\"\nandroidxCoreSplashscreen = \"1.0.1\"\nandroidxDataStore = \"1.1.1\"\nandroidxEspresso = \"3.6.1\"\nandroidxHiltNavigationCompose = \"1.2.0\"\nandroidxLifecycle = \"2.8.7\"\nandroidxMacroBenchmark = \"1.1.1\"\nandroidxMetrics = \"1.0.0-beta01\"\nandroidxNavigation = \"2.8.5\"\nandroidxProfileinstaller = \"1.4.1\"\nandroidxStartup = \"1.2.0\"\nandroidxTestCore = \"1.6.1\"\nandroidxTestExt = \"1.2.1\"\nandroidxTestRules = \"1.6.1\"\nandroidxTestRunner = \"1.6.2\"\nandroidxTracing = \"1.2.0\"\nandroidxUiAutomator = \"2.3.0\"\nandroidxWindowManager = \"1.3.0\"\nandroidxWork = \"2.10.0\"\ncoil = \"2.7.0\"\n# @keep\ncompileSdk = \"35\"\nhamcrest = \"1.3\"\nhilt = \"2.53.1\"\nhiltExt = \"1.2.0\"\njacoco = \"0.8.7\"\njunit4 = \"4.13.2\"\nkotlin = \"2.1.10\"\nkotlinxCoroutines = \"1.10.1\"\nkotlinxDatetime = \"0.6.2\"\nkotlinxSerializationJson = \"1.8.0\"\nksp = \"2.1.10-1.0.30\"\nlint = \"31.7.3\"\n# @keep\nminSdk = \"21\"\nokhttp = \"4.10.0\"\nprotobuf = \"3.21.12\"\nprotobufPlugin = \"0.9.4\"\nretrofit = \"2.9.0\"\nretrofitKotlinxSerializationJson = \"0.8.0\"\nroom = \"2.6.1\"\nspotless = \"5.12.5\"\ntimber = \"5.0.1\"\n# @keep\ntargetSdk = \"35\"\ntruth = \"1.4.4\"\nturbine = \"0.12.1\"\n\n[libraries]\naccompanist-appcompat-theme = { group = \"com.google.accompanist\", name = \"accompanist-appcompat-theme\", version.ref = \"accompanist\" }\naccompanist-flowlayout = { group = \"com.google.accompanist\", name = \"accompanist-flowlayout\", version.ref = \"accompanist\" }\naccompanist-swiperefresh = { group = \"com.google.accompanist\", name = \"accompanist-swiperefresh\", version.ref = \"accompanist\" }\naccompanist-systemuicontroller = { group = \"com.google.accompanist\", name = \"accompanist-systemuicontroller\", version.ref = \"accompanist\" }\naccompanist-testharness = { group = \"com.google.accompanist\", name = \"accompanist-testharness\", version.ref = \"accompanist\" }\nandroid-desugarJdkLibs = { group = \"com.android.tools\", name = \"desugar_jdk_libs\", version.ref = \"androidDesugarJdkLibs\" }\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"androidxActivity\" }\nandroidx-appcompat = { group = \"androidx.appcompat\", name = \"appcompat\", version.ref = \"androidxAppCompat\" }\nandroidx-archcore-testing = { group = \"androidx.arch.core\", name = \"core-testing\", version.ref = \"androidxArchCore\" }\nandroidx-annotation = { group = \"androidx.annotation\", name = \"annotation\", version.ref = \"annotation\" }\nandroidx-compose-animation = { group = \"androidx.compose.animation\", name = \"animation\" }\nandroidx-compose-bom = { group = \"androidx.compose\", name = \"compose-bom\", version.ref = \"androidxComposeBom\" }\nandroidx-compose-foundation-core = { group = \"androidx.compose.foundation\", name = \"foundation\" }\nandroidx-compose-foundation-layout = { group = \"androidx.compose.foundation\", name = \"foundation-layout\" }\nandroidx-compose-material-core = { group = \"androidx.compose.material\", name = \"material\" }\nandroidx-compose-material-iconsExtended = { group = \"androidx.compose.material\", name = \"material-icons-extended\" }\nandroidx-compose-material3 = { group = \"androidx.compose.material3\", name = \"material3\" }\nandroidx-compose-material3-windowSizeClass = { group = \"androidx.compose.material3\", name = \"material3-window-size-class\" }\nandroidx-compose-runtime = { group = \"androidx.compose.runtime\", name = \"runtime\" }\nandroidx-compose-runtime-livedata = { group = \"androidx.compose.runtime\", name = \"runtime-livedata\" }\nandroidx-compose-runtime-tracing = { group = \"androidx.compose.runtime\", name = \"runtime-tracing\", version.ref = \"androidxComposeRuntimeTracing\" }\nandroidx-compose-ui-test-junit = { group = \"androidx.compose.ui\", name = \"ui-test-junit4\" }\nandroidx-compose-ui-test-manifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\" }\nandroidx-compose-ui-tooling-core = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\nandroidx-compose-ui-tooling-preview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\" }\nandroidx-compose-ui-util = { group = \"androidx.compose.ui\", name = \"ui-util\" }\nandroidx-core-ktx = { group = \"androidx.core\", name = \"core-ktx\", version.ref = \"androidxCore\" }\nandroidx-core-splashscreen = { group = \"androidx.core\", name = \"core-splashscreen\", version.ref = \"androidxCoreSplashscreen\" }\nandroidx-dataStore-core = { group = \"androidx.datastore\", name = \"datastore\", version.ref = \"androidxDataStore\" }\nandroidx-dataStore-preferences = { group = \"androidx.datastore\", name = \"datastore-preferences\", version.ref = \"androidxDataStore\" }\nandroidx-hilt-navigation-compose = { group = \"androidx.hilt\", name = \"hilt-navigation-compose\", version.ref = \"androidxHiltNavigationCompose\" }\nandroidx-lifecycle-livedata-ktx = { group = \"androidx.lifecycle\", name = \"lifecycle-livedata-ktx\", version.ref = \"androidxLifecycle\" }\nandroidx-lifecycle-runtimeCompose = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-compose\", version.ref = \"androidxLifecycle\" }\nandroidx-lifecycle-viewModelCompose = { group = \"androidx.lifecycle\", name = \"lifecycle-viewmodel-compose\", version.ref = \"androidxLifecycle\" }\nandroidx-metrics = { group = \"androidx.metrics\", name = \"metrics-performance\", version.ref = \"androidxMetrics\" }\nandroidx-navigation-compose = { group = \"androidx.navigation\", name = \"navigation-compose\", version.ref = \"androidxNavigation\" }\nandroidx-navigation-testing = { group = \"androidx.navigation\", name = \"navigation-testing\", version.ref = \"androidxNavigation\" }\nandroidx-profileinstaller = { group = \"androidx.profileinstaller\", name = \"profileinstaller\", version.ref = \"androidxProfileinstaller\" }\nandroidx-startup = { group = \"androidx.startup\", name = \"startup-runtime\", version.ref = \"androidxStartup\" }\nandroidx-test-core-ktx = { group = \"androidx.test\", name = \"core-ktx\", version.ref = \"androidxTestCore\" }\nandroidx-test-espresso-core = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"androidxEspresso\" }\nandroidx-test-espresso-contrib = { group = \"androidx.test.espresso\", name = \"espresso-contrib\", version.ref = \"androidxEspresso\" }\nandroidx-test-espresso-intents = { group = \"androidx.test.espresso\", name = \"espresso-intents\", version.ref = \"androidxEspresso\" }\nandroidx-test-espresso-idling-resources = { group = \"androidx.test.espresso\", name = \"espresso-idling-resource\", version.ref = \"androidxEspresso\" }\nandroidx-test-espresso-idling-concurrent = { group = \"androidx.test.espresso.idling\", name = \"idling-concurrent\", version.ref = \"androidxEspresso\" }\nandroidx-test-ext = { group = \"androidx.test.ext\", name = \"junit-ktx\", version.ref = \"androidxTestExt\" }\nandroidx-test-rules = { group = \"androidx.test\", name = \"rules\", version.ref = \"androidxTestRules\" }\nandroidx-test-runner = { group = \"androidx.test\", name = \"runner\", version.ref = \"androidxTestRunner\" }\nandroidx-test-uiautomator = { group = \"androidx.test.uiautomator\", name = \"uiautomator\", version.ref = \"androidxUiAutomator\" }\nandroidx-tracing-ktx = { group = \"androidx.tracing\", name=\"tracing-ktx\", version.ref = \"androidxTracing\" }\nandroidx-window-manager = { module = \"androidx.window:window\", version.ref = \"androidxWindowManager\" }\nandroidx-work-ktx = { group = \"androidx.work\", name = \"work-runtime-ktx\", version.ref = \"androidxWork\" }\nandroidx-work-testing = { group = \"androidx.work\", name = \"work-testing\", version.ref = \"androidxWork\" }\ncoil-kt = { group = \"io.coil-kt\", name = \"coil\", version.ref = \"coil\" }\ncoil-kt-compose = { group = \"io.coil-kt\", name = \"coil-compose\", version.ref = \"coil\" }\ncoil-kt-svg = { group = \"io.coil-kt\", name = \"coil-svg\", version.ref = \"coil\" }\ngoogle-truth = { group = \"com.google.truth\", name = \"truth\", version.ref = \"truth\" }\nhamcrest = { group = \"org.hamcrest\", name = \"hamcrest-all\", version.ref = \"hamcrest\" }\nhilt-android-core = { group = \"com.google.dagger\", name = \"hilt-android\", version.ref = \"hilt\" }\nhilt-android-testing = { group = \"com.google.dagger\", name = \"hilt-android-testing\", version.ref = \"hilt\" }\nhilt-compiler = { group = \"com.google.dagger\", name = \"hilt-android-compiler\", version.ref = \"hilt\" }\nhilt-ext-compiler = { group = \"androidx.hilt\", name = \"hilt-compiler\", version.ref = \"hiltExt\" }\nhilt-ext-work = { group = \"androidx.hilt\", name = \"hilt-work\", version.ref = \"hiltExt\" }\njunit4 = { group = \"junit\", name = \"junit\", version.ref = \"junit4\" }\nkotlin-stdlib = { group = \"org.jetbrains.kotlin\", name = \"kotlin-stdlib-jdk8\", version.ref = \"kotlin\" }\nkotlinx-coroutines-android = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-coroutines-android\", version.ref = \"kotlinxCoroutines\" }\nkotlinx-coroutines-test = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-coroutines-test\", version.ref = \"kotlinxCoroutines\" }\nkotlinx-datetime = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-datetime\", version.ref = \"kotlinxDatetime\" }\nkotlinx-serialization-json = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-serialization-json\", version.ref = \"kotlinxSerializationJson\" }\nlint-api = { group = \"com.android.tools.lint\", name = \"lint-api\", version.ref = \"lint\" }\nroom-compiler = { group = \"androidx.room\", name = \"room-compiler\", version.ref = \"room\" }\nroom-ktx = { group = \"androidx.room\", name = \"room-ktx\", version.ref = \"room\" }\nroom-runtime = { group = \"androidx.room\", name = \"room-runtime\", version.ref = \"room\" }\nroom-testing = { group = \"androidx.room\", name = \"room-testing\", version.ref = \"room\" }\ntimber = { group = \"com.jakewharton.timber\", name = \"timber\", version.ref = \"timber\" }\n\n# Dependencies of the included build-logic\nandroid-gradlePlugin = { group = \"com.android.tools.build\", name = \"gradle\", version.ref = \"androidGradlePlugin\" }\nkotlin-gradlePlugin = { group = \"org.jetbrains.kotlin\", name = \"kotlin-gradle-plugin\", version.ref = \"kotlin\" }\n\n[plugins]\nandroid-application = { id = \"com.android.application\", version.ref = \"androidGradlePlugin\" }\nandroid-library = { id = \"com.android.library\", version.ref = \"androidGradlePlugin\" }\nandroid-test = { id = \"com.android.test\", version.ref = \"androidGradlePlugin\" }\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nhilt = { id = \"com.google.dagger.hilt.android\", version.ref = \"hilt\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlin-jvm = { id = \"org.jetbrains.kotlin.jvm\", version.ref = \"kotlin\" }\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\nksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nprotobuf = { id = \"com.google.protobuf\", version.ref = \"protobufPlugin\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx10248m -XX:MaxPermSize=256m\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\nandroid.enableJetifier=true\nandroid.useAndroidX=true\nksp.incremental.apt=true\norg.gradle.unsafe.configuration-cache=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045 \n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045 \n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:base\",\n    \"group:all\",\n    \":dependencyDashboard\",\n    \"schedule:daily\"\n  ],\n  \"baseBranches\": [\n    \"main\"\n  ],\n  \"commitMessageExtra\": \"{{{currentValue}}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}\",\n  \"packageRules\": [\n    {\n      \"matchPackagePatterns\": [\n        \"androidx.compose.compiler:compiler\"\n      ],\n      \"groupName\": \"kotlin\"\n    },\n    {\n      \"matchPackagePatterns\": [\n        \"org.jetbrains.kotlin.*\"\n      ],\n      \"groupName\": \"kotlin\"\n    },\n    {\n      \"matchPackagePatterns\": [\n        \"com.google.devtools.ksp\"\n      ],\n      \"groupName\": \"kotlin\"\n    }\n  ]\n}\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npluginManagement {\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\ninclude(\":app\")\ninclude(\":shared-test\")\n"
  },
  {
    "path": "shared-test/.gitignore",
    "content": "/build"
  },
  {
    "path": "shared-test/build.gradle.kts",
    "content": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nplugins {\n    alias(libs.plugins.android.library)\n    alias(libs.plugins.kotlin.android)\n    alias(libs.plugins.ksp)\n    alias(libs.plugins.hilt)\n}\n\nandroid {\n    namespace = \"com.example.android.architecture.blueprints.todoapp.shared.test\"\n    compileSdk = libs.versions.compileSdk.get().toInt()\n    defaultConfig {\n        minSdk = libs.versions.minSdk.get().toInt()\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n\n    kotlinOptions {\n        jvmTarget = \"17\"\n    }\n}\n\ndependencies {\n    implementation(project(\":app\"))\n    implementation(libs.kotlinx.coroutines.android)\n    implementation(libs.kotlinx.coroutines.test)\n    implementation(libs.junit4)\n    implementation(libs.androidx.test.core.ktx)\n    implementation(libs.androidx.test.ext)\n    implementation(libs.androidx.test.rules)\n    implementation(libs.hilt.android.core)\n    implementation(libs.hilt.android.testing)\n    ksp(libs.hilt.compiler)\n\n    // Room\n    implementation(libs.room.runtime)\n    implementation(libs.room.ktx)\n    ksp(libs.room.compiler)\n}\n"
  },
  {
    "path": "shared-test/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright 2023 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<manifest />\n"
  },
  {
    "path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/CustomTestRunner.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.test.runner.AndroidJUnitRunner\nimport dagger.hilt.android.testing.HiltTestApplication\n\nclass CustomTestRunner : AndroidJUnitRunner() {\n    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {\n        return super.newApplication(cl, HiltTestApplication::class.java.name, context)\n    }\n}\n"
  },
  {
    "path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/MainCoroutineRule.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.TestDispatcher\nimport kotlinx.coroutines.test.UnconfinedTestDispatcher\nimport kotlinx.coroutines.test.resetMain\nimport kotlinx.coroutines.test.setMain\nimport org.junit.rules.TestWatcher\nimport org.junit.runner.Description\n\n/**\n * Sets the main coroutines dispatcher to a [TestDispatcher] for unit testing.\n *\n * Declare it as a JUnit Rule:\n *\n * ```\n * @get:Rule\n * val mainCoroutineRule = MainCoroutineRule()\n * ```\n *\n * Then, use `runTest` to execute your tests.\n */\n@ExperimentalCoroutinesApi\nclass MainCoroutineRule(\n    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()\n) : TestWatcher() {\n\n    override fun starting(description: Description?) {\n        super.starting(description)\n        Dispatchers.setMain(testDispatcher)\n    }\n\n    override fun finished(description: Description?) {\n        super.finished(description)\n        Dispatchers.resetMain()\n    }\n}\n"
  },
  {
    "path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/FakeTaskRepository.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data\n\nimport androidx.annotation.VisibleForTesting\nimport java.util.UUID\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.update\n\n/**\n * Implementation of a tasks repository with static access to the data for easy testing.\n */\nclass FakeTaskRepository : TaskRepository {\n\n    private var shouldThrowError = false\n\n    private val _savedTasks = MutableStateFlow(LinkedHashMap<String, Task>())\n    val savedTasks: StateFlow<LinkedHashMap<String, Task>> = _savedTasks.asStateFlow()\n\n    private val observableTasks: Flow<List<Task>> = savedTasks.map {\n        if (shouldThrowError) {\n            throw Exception(\"Test exception\")\n        } else {\n            it.values.toList()\n        }\n    }\n\n    fun setShouldThrowError(value: Boolean) {\n        shouldThrowError = value\n    }\n\n    override suspend fun refresh() {\n        // Tasks already refreshed\n    }\n\n    override suspend fun refreshTask(taskId: String) {\n        refresh()\n    }\n\n    override suspend fun createTask(title: String, description: String): String {\n        val taskId = generateTaskId()\n        Task(title = title, description = description, id = taskId).also {\n            saveTask(it)\n        }\n        return taskId\n    }\n\n    override fun getTasksStream(): Flow<List<Task>> = observableTasks\n\n    override fun getTaskStream(taskId: String): Flow<Task?> {\n        return observableTasks.map { tasks ->\n            return@map tasks.firstOrNull { it.id == taskId }\n        }\n    }\n\n    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Task? {\n        if (shouldThrowError) {\n            throw Exception(\"Test exception\")\n        }\n        return savedTasks.value[taskId]\n    }\n\n    override suspend fun getTasks(forceUpdate: Boolean): List<Task> {\n        if (shouldThrowError) {\n            throw Exception(\"Test exception\")\n        }\n        return observableTasks.first()\n    }\n\n    override suspend fun updateTask(taskId: String, title: String, description: String) {\n        val updatedTask = _savedTasks.value[taskId]?.copy(\n            title = title,\n            description = description\n        ) ?: throw Exception(\"Task (id $taskId) not found\")\n\n        saveTask(updatedTask)\n    }\n\n    private fun saveTask(task: Task) {\n        _savedTasks.update { tasks ->\n            val newTasks = LinkedHashMap<String, Task>(tasks)\n            newTasks[task.id] = task\n            newTasks\n        }\n    }\n\n    override suspend fun completeTask(taskId: String) {\n        _savedTasks.value[taskId]?.let {\n            saveTask(it.copy(isCompleted = true))\n        }\n    }\n\n    override suspend fun activateTask(taskId: String) {\n        _savedTasks.value[taskId]?.let {\n            saveTask(it.copy(isCompleted = false))\n        }\n    }\n\n    override suspend fun clearCompletedTasks() {\n        _savedTasks.update { tasks ->\n            tasks.filterValues {\n                !it.isCompleted\n            } as LinkedHashMap<String, Task>\n        }\n    }\n\n    override suspend fun deleteTask(taskId: String) {\n        _savedTasks.update { tasks ->\n            val newTasks = LinkedHashMap<String, Task>(tasks)\n            newTasks.remove(taskId)\n            newTasks\n        }\n    }\n\n    override suspend fun deleteAllTasks() {\n        _savedTasks.update {\n            LinkedHashMap()\n        }\n    }\n\n    private fun generateTaskId() = UUID.randomUUID().toString()\n\n    @VisibleForTesting\n    fun addTasks(vararg tasks: Task) {\n        _savedTasks.update { oldTasks ->\n            val newTasks = LinkedHashMap<String, Task>(oldTasks)\n            for (task in tasks) {\n                newTasks[task.id] = task\n            }\n            newTasks\n        }\n    }\n}\n"
  },
  {
    "path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/local/FakeTaskDao.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.local\n\nimport kotlinx.coroutines.flow.Flow\n\nclass FakeTaskDao(initialTasks: List<LocalTask>? = emptyList()) : TaskDao {\n\n    private var _tasks: MutableMap<String, LocalTask>? = null\n\n    var tasks: List<LocalTask>?\n        get() = _tasks?.values?.toList()\n        set(newTasks) {\n            _tasks = newTasks?.associateBy { it.id }?.toMutableMap()\n        }\n\n    init {\n        tasks = initialTasks\n    }\n\n    override suspend fun getAll() = tasks ?: throw Exception(\"Task list is null\")\n\n    override suspend fun getById(taskId: String): LocalTask? = _tasks?.get(taskId)\n\n    override suspend fun upsertAll(tasks: List<LocalTask>) {\n        _tasks?.putAll(tasks.associateBy { it.id })\n    }\n\n    override suspend fun upsert(task: LocalTask) {\n        _tasks?.put(task.id, task)\n    }\n\n    override suspend fun updateCompleted(taskId: String, completed: Boolean) {\n        _tasks?.get(taskId)?.let { it.isCompleted = completed }\n    }\n\n    override suspend fun deleteAll() {\n        _tasks?.clear()\n    }\n\n    override suspend fun deleteById(taskId: String): Int {\n        return if (_tasks?.remove(taskId) == null) {\n            0\n        } else {\n            1\n        }\n    }\n\n    override suspend fun deleteCompleted(): Int {\n        _tasks?.apply {\n            val originalSize = size\n            entries.removeIf { it.value.isCompleted }\n            return originalSize - size\n        }\n        return 0\n    }\n\n    override fun observeAll(): Flow<List<LocalTask>> {\n        TODO(\"Not implemented\")\n    }\n\n    override fun observeById(taskId: String): Flow<LocalTask> {\n        TODO(\"Not implemented\")\n    }\n}\n"
  },
  {
    "path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/data/source/network/FakeNetworkDataSource.kt",
    "content": "/*\n * Copyright 2019 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.data.source.network\n\nclass FakeNetworkDataSource(\n    var tasks: MutableList<NetworkTask>? = mutableListOf()\n) : NetworkDataSource {\n    override suspend fun loadTasks() = tasks ?: throw Exception(\"Task list is null\")\n\n    override suspend fun saveTasks(tasks: List<NetworkTask>) {\n        this.tasks = tasks.toMutableList()\n    }\n}\n"
  },
  {
    "path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/DatabaseTestModule.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.di\n\nimport android.content.Context\nimport androidx.room.Room\nimport com.example.android.architecture.blueprints.todoapp.data.source.local.ToDoDatabase\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport dagger.hilt.testing.TestInstallIn\nimport javax.inject.Singleton\n\n@Module\n@TestInstallIn(\n    components = [SingletonComponent::class],\n    replaces = [DatabaseModule::class]\n)\nobject DatabaseTestModule {\n\n    @Singleton\n    @Provides\n    fun provideDataBase(@ApplicationContext context: Context): ToDoDatabase {\n        return Room\n            .inMemoryDatabaseBuilder(context.applicationContext, ToDoDatabase::class.java)\n            .allowMainThreadQueries()\n            .build()\n    }\n}\n"
  },
  {
    "path": "shared-test/src/main/java/com/example/android/architecture/blueprints/todoapp/di/RepositoryTestModule.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.example.android.architecture.blueprints.todoapp.di\n\nimport com.example.android.architecture.blueprints.todoapp.data.FakeTaskRepository\nimport com.example.android.architecture.blueprints.todoapp.data.TaskRepository\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.components.SingletonComponent\nimport dagger.hilt.testing.TestInstallIn\nimport javax.inject.Singleton\n\n@Module\n@TestInstallIn(\n    components = [SingletonComponent::class],\n    replaces = [RepositoryModule::class]\n)\nobject RepositoryTestModule {\n\n    @Singleton\n    @Provides\n    fun provideTasksRepository(): TaskRepository {\n        return FakeTaskRepository()\n    }\n}\n"
  },
  {
    "path": "spotless/copyright.kt",
    "content": "/*\n * Copyright $YEAR The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n"
  },
  {
    "path": "spotless/copyright.kts",
    "content": "/*\n * Copyright $YEAR The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"
  },
  {
    "path": "spotless/copyright.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n     Copyright $YEAR The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n"
  }
]