├── .github ├── ISSUE_TEMPLATE │ ├── advanced-workmanager-and-testing.md │ └── background-work-with-workmanager.md ├── renovate.json └── workflows │ └── main.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── WorkerInstrumentationTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── bluromatic │ │ ├── BlurActivity.kt │ │ ├── BluromaticApplication.kt │ │ ├── Constants.kt │ │ ├── data │ │ ├── AppContainer.kt │ │ ├── BlurAmount.kt │ │ ├── BlurAmountData.kt │ │ ├── BluromaticRepository.kt │ │ └── WorkManagerBluromaticRepository.kt │ │ ├── domain │ │ ├── CreateImageUriUseCase.kt │ │ └── SaveImageUseCase.kt │ │ ├── ui │ │ ├── BlurViewModel.kt │ │ ├── BluromaticScreen.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ └── Theme.kt │ │ └── workers │ │ ├── BlurWorker.kt │ │ ├── CleanupWorker.kt │ │ ├── SaveImageToFileWorker.kt │ │ └── WorkerUtils.kt │ └── res │ ├── drawable-nodpi │ └── android_cupcake.png │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/ISSUE_TEMPLATE/advanced-workmanager-and-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Advanced WorkManager and Testing codelab issue template' 3 | about: Issue template for Advanced WorkManager and Testing codelab 4 | title: 'Codelab: Advanced WorkManager and Testing' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **URL of codelab** 11 | 12 | **Specify the language of the codelab if it is not English:** 13 | 14 | **In which task and step of the codelab can this issue be found?** 15 | 16 | 17 | **Describe the problem** 18 | 19 | 20 | 21 | 22 | **Steps to reproduce?** 23 | 1. Go to... 24 | 2. Click on... 25 | 3. See error... 26 | 27 | **Versions** 28 | _Android Studio version:_ 29 | _API version of the emulator:_ 30 | 31 | 32 | **Additional information** 33 | _Include screenshots if they would be useful in clarifying the problem._ 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/background-work-with-workmanager.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Background Work with WorkManager codelab issue template' 3 | about: Issue template for Background Work with WorkManager codelab 4 | title: 'Codelab: Background Work with WorkManager' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **URL of codelab** 11 | 12 | **Specify the language of the codelab if it is not English:** 13 | 14 | **In which task and step of the codelab can this issue be found?** 15 | 16 | 17 | **Describe the problem** 18 | 19 | 20 | 21 | 22 | **Steps to reproduce?** 23 | 1. Go to... 24 | 2. Click on... 25 | 3. See error... 26 | 27 | **Versions** 28 | _Android Studio version:_ 29 | _API version of the emulator:_ 30 | 31 | 32 | **Additional information** 33 | _Include screenshots if they would be useful in clarifying the problem._ 34 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>android/.github:renovate-config" 5 | ], 6 | 7 | "baseBranches": [ 8 | "main", 9 | "starter", 10 | "intermediate" 11 | ] 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - starter 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | strategy: 14 | matrix: 15 | api-level: [29] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Enable KVM group perms 21 | run: | 22 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 23 | sudo udevadm control --reload-rules 24 | sudo udevadm trigger --name-match=kvm 25 | ls /dev/kvm 26 | 27 | - name: Set Up JDK 28 | uses: actions/setup-java@v4 29 | with: 30 | distribution: 'zulu' # See 'Supported distributions' for available options 31 | java-version: '17' 32 | cache: 'gradle' 33 | 34 | - name: Setup Gradle 35 | uses: gradle/actions/setup-gradle@v4 36 | 37 | - name: Setup Android SDK 38 | uses: android-actions/setup-android@v3 39 | 40 | - name: Build project and run local tests 41 | run: ./gradlew :app:test 42 | 43 | - name: Run instrumentation tests 44 | uses: reactivecircus/android-emulator-runner@v2 45 | with: 46 | api-level: ${{ matrix.api-level }} 47 | arch: x86 48 | disable-animations: true 49 | script: ./gradlew :app:connectedCheck --stacktrace 50 | 51 | - name: Upload test reports 52 | if: always() 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: test-reports-${{ matrix.api-level }} 56 | path: ./app/build/reports/androidTests 57 | 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Mac files 6 | .DS_Store 7 | 8 | # files for the dex VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # generated files 15 | bin/ 16 | gen/ 17 | 18 | # Ignore gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | proguard-project.txt 28 | 29 | # Eclipse files 30 | .project 31 | .classpath 32 | .settings/ 33 | 34 | # Android Studio/IDEA 35 | *.iml 36 | .idea 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your patches! Before we can take them, we have to jump a couple of 6 | legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement (CLA). 9 | 10 | * If you are an individual writing original source code and you're sure you 11 | own the intellectual property, then you'll need to sign an [individual CLA] 12 | (https://cla.developers.google.com). 13 | * If you work for a company that wants to allow you to contribute your work, 14 | then you'll need to sign a [corporate CLA] 15 | (https://cla.developers.google.com). 16 | * Please make sure you sign both, Android and Google CLA 17 | 18 | Follow either of the two links above to access the appropriate CLA and 19 | instructions for how to sign and return it. Once we receive it, we'll be able to 20 | accept your pull requests. 21 | 22 | ## Contributing A Patch 23 | 24 | 1. Submit an issue describing your proposed change to the repo in question. 25 | 1. The repo owner will respond to your issue promptly. 26 | 1. If your proposed change is accepted, and you haven't already done so, sign a 27 | Contributor License Agreement (see details above). 28 | 1. Fork the desired repo, develop and test your code changes. 29 | 1. Ensure that your code adheres to the existing style in the sample to which 30 | you are contributing. Refer to the 31 | [Android Code Style Guide] 32 | (https://source.android.com/source/code-style.html) for the 33 | recommended coding standards for this organization. 34 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 35 | 1. Submit a pull request. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WorkManager Codelab 2 | =================================== 3 | 4 | This repository contains the code for the [WorkManager Codelab](https://developer.android.com/codelabs/basic-android-kotlin-compose-workmanager). 5 | 6 | Introduction 7 | ------------ 8 | 9 | At I/O 2018, Google announced [Android Jetpack](https://developer.android.com/jetpack/), a collection of libraries, tools, and architectural guidance to accelerate and simplify the development of great Android apps. One of those libraries is the [WorkManager library](https://developer.android.com/topic/libraries/architecture/workmanager/). The WorkManager library provides a unified API for deferrable one-off or recurring background tasks that need guaranteed execution. You can learn more by reading the [WorkManager Guide](https://developer.android.com/topic/libraries/architecture/workmanager/), the [WorkManager Reference](https://developer.android.com/reference/androidx/work/package-summary) or doing the [WorkManager Codelab](https://developer.android.com/codelabs/basic-android-kotlin-compose-workmanager). 10 | 11 | Pre-requisites 12 | -------------- 13 | 14 | * Familiarity with how to open, build, and run apps with Android Studio. 15 | 16 | * Make sure Android Studio is updated, as well as your SDK and Gradle. Otherwise, you may have to wait for a while until all the updates are done. 17 | 18 | * A device or emulator that runs API level 21+ 19 | 20 | You need to be solidly familiar with the Kotlin programming language, object-oriented design concepts, and Android Development Fundamentals. 21 | 22 | In particular: 23 | 24 | * Basics of [Jetpack Compose](https://developer.android.com/courses/pathways/compose) 25 | * Some familiarity with URIs and File I/O 26 | * Familiarity with [Kotlin Flow](https://developer.android.com/kotlin/flow) and [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) 27 | 28 | Getting Started 29 | --------------- 30 | 31 | 1. [Install Android Studio](https://developer.android.com/studio/install.html), if you don't already have it. 32 | 2. Download the sample. 33 | 3. Import the sample into Android Studio. 34 | 4. Build and run the sample. 35 | 36 | Notes: 37 | - The application code contains a battery not low constraint. If the device/emulator has a low battery, the application will appear to hang until this constraint is met. 38 | 39 | - The app requires notifications to be enabled. To enable notifications, navigate to the Android Settings menu > Apps > Blur-O-Matic > Notifications > Enable "All Blur-O-Matic notifications". 40 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("com.android.application") 19 | id("org.jetbrains.kotlin.android") 20 | id("org.jetbrains.kotlin.plugin.compose") 21 | } 22 | 23 | android { 24 | namespace = "com.example.bluromatic" 25 | compileSdk = 35 26 | 27 | defaultConfig { 28 | applicationId = "com.example.bluromatic" 29 | minSdk = 24 30 | targetSdk = 35 31 | versionCode = 1 32 | versionName = "1.0" 33 | 34 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 35 | vectorDrawables { 36 | useSupportLibrary = true 37 | } 38 | } 39 | 40 | buildTypes { 41 | release { 42 | isMinifyEnabled = false 43 | proguardFiles( 44 | getDefaultProguardFile("proguard-android-optimize.txt"), 45 | "proguard-rules.pro" 46 | ) 47 | } 48 | } 49 | compileOptions { 50 | sourceCompatibility = JavaVersion.VERSION_1_8 51 | targetCompatibility = JavaVersion.VERSION_1_8 52 | } 53 | kotlinOptions { 54 | jvmTarget = "1.8" 55 | } 56 | buildFeatures { 57 | compose = true 58 | } 59 | packaging { 60 | resources { 61 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 62 | } 63 | } 64 | } 65 | 66 | dependencies { 67 | 68 | implementation(platform("androidx.compose:compose-bom:2024.12.01")) 69 | implementation("androidx.activity:activity-compose:1.9.3") 70 | implementation("androidx.activity:activity-ktx:1.9.3") 71 | implementation("androidx.appcompat:appcompat:1.7.0") 72 | implementation("androidx.compose.material3:material3") 73 | implementation("androidx.compose.runtime:runtime") 74 | implementation("androidx.compose.runtime:runtime-livedata") 75 | implementation("androidx.compose.ui:ui") 76 | implementation("androidx.compose.ui:ui-graphics") 77 | implementation("androidx.compose.ui:ui-tooling-preview") 78 | implementation("androidx.core:core-ktx:1.15.0") 79 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:${rootProject.extra["lifecycle_version"]}") 80 | implementation("androidx.lifecycle:lifecycle-runtime-compose:${rootProject.extra["lifecycle_version"]}") 81 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:${rootProject.extra["lifecycle_version"]}") 82 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${rootProject.extra["lifecycle_version"]}") 83 | implementation("androidx.work:work-runtime-ktx:2.10.0") 84 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 85 | androidTestImplementation("androidx.test.ext:junit:1.2.1") 86 | debugImplementation("androidx.compose.ui:ui-test-manifest") 87 | debugImplementation("androidx.compose.ui:ui-tooling") 88 | 89 | // Work testing 90 | androidTestImplementation("androidx.work:work-testing:2.10.0") 91 | 92 | } 93 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/WorkerInstrumentationTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import android.content.Context 18 | import android.os.Build 19 | import androidx.test.core.app.ApplicationProvider 20 | import androidx.work.ListenableWorker 21 | import androidx.work.testing.TestListenableWorkerBuilder 22 | import androidx.work.workDataOf 23 | import com.example.bluromatic.KEY_IMAGE_URI 24 | import com.example.bluromatic.workers.BlurWorker 25 | import com.example.bluromatic.workers.CleanupWorker 26 | import com.example.bluromatic.workers.SaveImageToFileWorker 27 | import junit.framework.TestCase 28 | import kotlinx.coroutines.runBlocking 29 | import org.junit.Assert.assertTrue 30 | import org.junit.Before 31 | import org.junit.Test 32 | 33 | class WorkerInstrumentationTest { 34 | private lateinit var context: Context 35 | private val mockUriInput: Pair = 36 | KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake" 37 | 38 | @Before 39 | fun setUp() { 40 | context = ApplicationProvider.getApplicationContext() 41 | } 42 | 43 | @Test 44 | fun cleanupWorker_doWork_resultSuccess() { 45 | val worker = TestListenableWorkerBuilder(context).build() 46 | runBlocking { 47 | val result = worker.doWork() 48 | assertTrue(result is ListenableWorker.Result.Success) 49 | } 50 | } 51 | 52 | @Test 53 | fun blurWorker_doWork_resultSuccessReturnsUri() { 54 | val worker = TestListenableWorkerBuilder(context) 55 | .setInputData(workDataOf(mockUriInput)) 56 | .build() 57 | runBlocking { 58 | val result = worker.doWork() 59 | val resultUri = result.outputData.getString(KEY_IMAGE_URI) 60 | assertTrue(result is ListenableWorker.Result.Success) 61 | assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI)) 62 | assertTrue( 63 | resultUri?.startsWith("file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-") 64 | ?: false 65 | ) 66 | } 67 | 68 | } 69 | 70 | @Test 71 | fun saveImageToFileWorker_doWork_resultSuccessReturnsUrl() { 72 | val worker = TestListenableWorkerBuilder(context) 73 | .setInputData(workDataOf(mockUriInput)) 74 | .build() 75 | runBlocking { 76 | val result = worker.doWork() 77 | val resultUri = result.outputData.getString(KEY_IMAGE_URI) 78 | assertTrue(result is ListenableWorker.Result.Success) 79 | assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI)) 80 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 81 | TestCase.assertTrue( 82 | resultUri?.startsWith("content://media/external_primary/images/media/") 83 | ?: false 84 | ) 85 | } else { 86 | TestCase.assertTrue( 87 | resultUri?.startsWith("content://media/external/images/media/") 88 | ?: false 89 | ) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/BlurActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic 18 | 19 | import android.content.ContentResolver 20 | import android.content.Context 21 | import android.net.Uri 22 | import android.os.Bundle 23 | import androidx.activity.ComponentActivity 24 | import androidx.activity.compose.setContent 25 | import androidx.activity.enableEdgeToEdge 26 | import androidx.compose.foundation.layout.fillMaxSize 27 | import androidx.compose.material3.Surface 28 | import androidx.compose.ui.Modifier 29 | import com.example.bluromatic.ui.BluromaticScreen 30 | import com.example.bluromatic.ui.theme.BluromaticTheme 31 | 32 | class BlurActivity : ComponentActivity() { 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | enableEdgeToEdge() 35 | super.onCreate(savedInstanceState) 36 | setContent { 37 | BluromaticTheme { 38 | Surface( 39 | modifier = Modifier.fillMaxSize() 40 | ) { 41 | BluromaticScreen() 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | fun Context.getImageUri(): Uri { 49 | val resources = this.resources 50 | 51 | return Uri.Builder() 52 | .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 53 | .authority(resources.getResourcePackageName(R.drawable.android_cupcake)) 54 | .appendPath(resources.getResourceTypeName(R.drawable.android_cupcake)) 55 | .appendPath(resources.getResourceEntryName(R.drawable.android_cupcake)) 56 | .build() 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/BluromaticApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic 18 | 19 | import android.app.Application 20 | import com.example.bluromatic.data.AppContainer 21 | import com.example.bluromatic.data.DefaultAppContainer 22 | 23 | class BluromaticApplication : Application() { 24 | /** AppContainer instance used by the rest of classes to obtain dependencies */ 25 | lateinit var container: AppContainer 26 | override fun onCreate() { 27 | super.onCreate() 28 | container = DefaultAppContainer(this) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/Constants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic 18 | 19 | // Notification Channel constants 20 | 21 | // Name of Notification Channel for verbose notifications of background work 22 | val VERBOSE_NOTIFICATION_CHANNEL_NAME: CharSequence = 23 | "Verbose WorkManager Notifications" 24 | const val VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION = 25 | "Shows notifications whenever work starts" 26 | val NOTIFICATION_TITLE: CharSequence = "WorkRequest Starting" 27 | const val CHANNEL_ID = "VERBOSE_NOTIFICATION" 28 | const val NOTIFICATION_ID = 1 29 | 30 | // The name of the image manipulation work 31 | const val IMAGE_MANIPULATION_WORK_NAME = "image_manipulation_work" 32 | 33 | // Other keys 34 | const val OUTPUT_PATH = "blur_filter_outputs" 35 | const val KEY_IMAGE_URI = "KEY_IMAGE_URI" 36 | const val TAG_OUTPUT = "OUTPUT" 37 | const val KEY_BLUR_LEVEL = "KEY_BLUR_LEVEL" 38 | 39 | const val DELAY_TIME_MILLIS: Long = 3000 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/data/AppContainer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.data 18 | 19 | import android.content.Context 20 | 21 | interface AppContainer { 22 | val bluromaticRepository: BluromaticRepository 23 | } 24 | 25 | class DefaultAppContainer(context: Context) : AppContainer { 26 | override val bluromaticRepository = WorkManagerBluromaticRepository(context) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/data/BlurAmount.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.data 18 | 19 | import androidx.annotation.StringRes 20 | 21 | data class BlurAmount( 22 | @StringRes val blurAmountRes: Int, 23 | val blurAmount: Int 24 | ) 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/data/BlurAmountData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.data 18 | 19 | import com.example.bluromatic.R 20 | 21 | object BlurAmountData { 22 | val blurAmount = listOf( 23 | BlurAmount( 24 | blurAmountRes = R.string.blur_lv_1, 25 | blurAmount = 1 26 | ), 27 | BlurAmount( 28 | blurAmountRes = R.string.blur_lv_2, 29 | blurAmount = 2 30 | ), 31 | BlurAmount( 32 | blurAmountRes = R.string.blur_lv_3, 33 | blurAmount = 3 34 | ) 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/data/BluromaticRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.data 18 | 19 | import androidx.work.WorkInfo 20 | import kotlinx.coroutines.flow.Flow 21 | 22 | interface BluromaticRepository { 23 | val outputWorkInfo: Flow 24 | fun applyBlur(blurLevel: Int) 25 | fun cancelWork() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/data/WorkManagerBluromaticRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.data 18 | 19 | import android.content.Context 20 | import android.net.Uri 21 | import androidx.lifecycle.asFlow 22 | import androidx.work.Constraints 23 | import androidx.work.Data 24 | import androidx.work.ExistingWorkPolicy 25 | import androidx.work.OneTimeWorkRequest 26 | import androidx.work.OneTimeWorkRequestBuilder 27 | import androidx.work.WorkInfo 28 | import androidx.work.WorkManager 29 | import com.example.bluromatic.IMAGE_MANIPULATION_WORK_NAME 30 | import com.example.bluromatic.KEY_BLUR_LEVEL 31 | import com.example.bluromatic.KEY_IMAGE_URI 32 | import com.example.bluromatic.TAG_OUTPUT 33 | import com.example.bluromatic.getImageUri 34 | import com.example.bluromatic.workers.BlurWorker 35 | import com.example.bluromatic.workers.CleanupWorker 36 | import com.example.bluromatic.workers.SaveImageToFileWorker 37 | import kotlinx.coroutines.flow.Flow 38 | import kotlinx.coroutines.flow.mapNotNull 39 | 40 | class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository { 41 | 42 | private var imageUri: Uri = context.getImageUri() 43 | private val workManager = WorkManager.getInstance(context) 44 | 45 | override val outputWorkInfo: Flow = 46 | workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow().mapNotNull { 47 | if (it.isNotEmpty()) it.first() else null 48 | } 49 | 50 | /** 51 | * Create the WorkRequests to apply the blur and save the resulting image 52 | * @param blurLevel The amount to blur the image 53 | */ 54 | override fun applyBlur(blurLevel: Int) { 55 | // Add WorkRequest to Cleanup temporary images 56 | var continuation = workManager 57 | .beginUniqueWork( 58 | IMAGE_MANIPULATION_WORK_NAME, 59 | ExistingWorkPolicy.REPLACE, 60 | OneTimeWorkRequest.from(CleanupWorker::class.java) 61 | ) 62 | 63 | // Create low battery constraint 64 | val constraints = Constraints.Builder() 65 | .setRequiresBatteryNotLow(true) 66 | .build() 67 | 68 | // Add WorkRequest to blur the image 69 | val blurBuilder = OneTimeWorkRequestBuilder() 70 | 71 | // Input the Uri for the blur operation along with the blur level 72 | blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri)) 73 | 74 | blurBuilder.setConstraints(constraints) 75 | 76 | continuation = continuation.then(blurBuilder.build()) 77 | 78 | // Add WorkRequest to save the image to the filesystem 79 | val save = OneTimeWorkRequestBuilder() 80 | .addTag(TAG_OUTPUT) 81 | .build() 82 | continuation = continuation.then(save) 83 | 84 | // Actually start the work 85 | continuation.enqueue() 86 | } 87 | 88 | /** 89 | * Cancel any ongoing WorkRequests 90 | * */ 91 | override fun cancelWork() { 92 | workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME) 93 | } 94 | 95 | /** 96 | * Creates the input data bundle which includes the blur level to 97 | * update the amount of blur to be applied and the Uri to operate on 98 | * @return Data which contains the Image Uri as a String and blur level as an Integer 99 | */ 100 | private fun createInputDataForWorkRequest(blurLevel: Int, imageUri: Uri): Data { 101 | val builder = Data.Builder() 102 | builder.putString(KEY_IMAGE_URI, imageUri.toString()).putInt(KEY_BLUR_LEVEL, blurLevel) 103 | return builder.build() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/domain/CreateImageUriUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.bluromatic.domain 2 | 3 | import android.content.ContentResolver 4 | import android.content.ContentValues 5 | import android.net.Uri 6 | import android.provider.MediaStore 7 | import androidx.annotation.RequiresApi 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | 11 | /** 12 | * Create imageUri. 13 | */ 14 | @RequiresApi(29) 15 | class CreateImageUriUseCase { 16 | suspend operator fun invoke(resolver: ContentResolver, filename: String): Uri? { 17 | return withContext(Dispatchers.IO) { 18 | val imageCollection = 19 | MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) 20 | val values = ContentValues().apply { 21 | put(MediaStore.Images.Media.DISPLAY_NAME, filename) 22 | put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Blur-O-Matic") 23 | put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") 24 | put(MediaStore.Images.Media.IS_PENDING, 1) 25 | } 26 | val imageUri = resolver.insert(imageCollection, values) 27 | imageUri 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/domain/SaveImageUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.example.bluromatic.domain 2 | 3 | import android.content.ContentResolver 4 | import android.content.ContentValues 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.provider.MediaStore 8 | import android.util.Log 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.withContext 11 | 12 | private const val TAG = "SaveImage" 13 | 14 | /** 15 | * Save image into MediaStore. 16 | */ 17 | class SaveImageUseCase { 18 | suspend operator fun invoke( 19 | resolver: ContentResolver, 20 | contentUri: Uri?, 21 | bitmap: Bitmap, 22 | ) = withContext(Dispatchers.IO) { 23 | try { 24 | contentUri?.let { contentUri -> 25 | resolver.openOutputStream(contentUri, "w")?.use { outputStream -> 26 | bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outputStream) 27 | } 28 | val values = ContentValues().apply { 29 | put(MediaStore.Images.Media.IS_PENDING, 0) 30 | } 31 | resolver.update(contentUri, values, null, null) 32 | } 33 | } catch (e: Throwable) { 34 | Log.e(TAG, "Error occurs $e") 35 | throw e 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/ui/BlurViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.ui 18 | 19 | import androidx.lifecycle.ViewModel 20 | import androidx.lifecycle.ViewModelProvider 21 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 22 | import androidx.lifecycle.viewModelScope 23 | import androidx.lifecycle.viewmodel.initializer 24 | import androidx.lifecycle.viewmodel.viewModelFactory 25 | import androidx.work.WorkInfo 26 | import com.example.bluromatic.BluromaticApplication 27 | import com.example.bluromatic.KEY_IMAGE_URI 28 | import com.example.bluromatic.data.BlurAmountData 29 | import com.example.bluromatic.data.BluromaticRepository 30 | import kotlinx.coroutines.flow.SharingStarted 31 | import kotlinx.coroutines.flow.StateFlow 32 | import kotlinx.coroutines.flow.map 33 | import kotlinx.coroutines.flow.stateIn 34 | 35 | /** 36 | * [BlurViewModel] starts and stops the WorkManger and applies blur to the image. Also updates the 37 | * visibility states of the buttons depending on the states of the WorkManger. 38 | */ 39 | class BlurViewModel(private val bluromaticRepository: BluromaticRepository) : ViewModel() { 40 | 41 | internal val blurAmount = BlurAmountData.blurAmount 42 | 43 | val blurUiState: StateFlow = bluromaticRepository.outputWorkInfo 44 | .map { info -> 45 | val outputImageUri = info.outputData.getString(KEY_IMAGE_URI) 46 | when { 47 | info.state.isFinished && !outputImageUri.isNullOrEmpty() -> { 48 | BlurUiState.Complete(outputUri = outputImageUri) 49 | } 50 | info.state == WorkInfo.State.CANCELLED -> { 51 | BlurUiState.Default 52 | } 53 | else -> BlurUiState.Loading 54 | } 55 | }.stateIn( 56 | scope = viewModelScope, 57 | started = SharingStarted.WhileSubscribed(5_000), 58 | initialValue = BlurUiState.Default 59 | ) 60 | 61 | /** 62 | * Call the method from repository to create the WorkRequest to apply the blur 63 | * and save the resulting image 64 | * @param blurLevel The amount to blur the image 65 | */ 66 | fun applyBlur(blurLevel: Int) { 67 | bluromaticRepository.applyBlur(blurLevel) 68 | } 69 | 70 | /** 71 | * Call method from repository to cancel any ongoing WorkRequest 72 | * */ 73 | fun cancelWork() { 74 | bluromaticRepository.cancelWork() 75 | } 76 | 77 | /** 78 | * Factory for [BlurViewModel] that takes [BluromaticRepository] as a dependency 79 | */ 80 | companion object { 81 | val Factory: ViewModelProvider.Factory = viewModelFactory { 82 | initializer { 83 | val bluromaticRepository = 84 | (this[APPLICATION_KEY] as BluromaticApplication).container.bluromaticRepository 85 | BlurViewModel( 86 | bluromaticRepository = bluromaticRepository 87 | ) 88 | } 89 | } 90 | } 91 | } 92 | 93 | sealed interface BlurUiState { 94 | object Default : BlurUiState 95 | object Loading : BlurUiState 96 | data class Complete(val outputUri: String) : BlurUiState 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/ui/BluromaticScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.ui 18 | 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.net.Uri 22 | import androidx.compose.foundation.Image 23 | import androidx.compose.foundation.layout.Arrangement 24 | import androidx.compose.foundation.layout.Column 25 | import androidx.compose.foundation.layout.Row 26 | import androidx.compose.foundation.layout.Spacer 27 | import androidx.compose.foundation.layout.WindowInsets 28 | import androidx.compose.foundation.layout.asPaddingValues 29 | import androidx.compose.foundation.layout.calculateEndPadding 30 | import androidx.compose.foundation.layout.calculateStartPadding 31 | import androidx.compose.foundation.layout.fillMaxSize 32 | import androidx.compose.foundation.layout.fillMaxWidth 33 | import androidx.compose.foundation.layout.height 34 | import androidx.compose.foundation.layout.padding 35 | import androidx.compose.foundation.layout.safeDrawing 36 | import androidx.compose.foundation.layout.size 37 | import androidx.compose.foundation.layout.statusBarsPadding 38 | import androidx.compose.foundation.layout.width 39 | import androidx.compose.foundation.rememberScrollState 40 | import androidx.compose.foundation.selection.selectable 41 | import androidx.compose.foundation.selection.selectableGroup 42 | import androidx.compose.foundation.verticalScroll 43 | import androidx.compose.material3.Button 44 | import androidx.compose.material3.CircularProgressIndicator 45 | import androidx.compose.material3.FilledTonalButton 46 | import androidx.compose.material3.MaterialTheme 47 | import androidx.compose.material3.RadioButton 48 | import androidx.compose.material3.RadioButtonDefaults 49 | import androidx.compose.material3.Surface 50 | import androidx.compose.material3.Text 51 | import androidx.compose.runtime.Composable 52 | import androidx.compose.runtime.getValue 53 | import androidx.compose.runtime.mutableStateOf 54 | import androidx.compose.runtime.saveable.rememberSaveable 55 | import androidx.compose.runtime.setValue 56 | import androidx.compose.ui.Alignment 57 | import androidx.compose.ui.Modifier 58 | import androidx.compose.ui.layout.ContentScale 59 | import androidx.compose.ui.platform.LocalContext 60 | import androidx.compose.ui.platform.LocalLayoutDirection 61 | import androidx.compose.ui.res.dimensionResource 62 | import androidx.compose.ui.res.painterResource 63 | import androidx.compose.ui.res.stringResource 64 | import androidx.compose.ui.semantics.Role 65 | import androidx.compose.ui.tooling.preview.Preview 66 | import androidx.compose.ui.unit.dp 67 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 68 | import androidx.lifecycle.viewmodel.compose.viewModel 69 | import com.example.bluromatic.R 70 | import com.example.bluromatic.data.BlurAmount 71 | import com.example.bluromatic.ui.theme.BluromaticTheme 72 | 73 | @Composable 74 | fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) { 75 | val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle() 76 | val layoutDirection = LocalLayoutDirection.current 77 | Surface( 78 | modifier = Modifier 79 | .fillMaxSize() 80 | .statusBarsPadding() 81 | .padding( 82 | start = WindowInsets.safeDrawing.asPaddingValues() 83 | .calculateStartPadding(layoutDirection), 84 | end = WindowInsets.safeDrawing.asPaddingValues() 85 | .calculateEndPadding(layoutDirection) 86 | ) 87 | ) { 88 | BluromaticScreenContent( 89 | blurUiState = uiState, 90 | blurAmountOptions = blurViewModel.blurAmount, 91 | applyBlur = blurViewModel::applyBlur, 92 | cancelWork = blurViewModel::cancelWork, 93 | modifier = Modifier 94 | .verticalScroll(rememberScrollState()) 95 | .padding(dimensionResource(R.dimen.padding_medium)) 96 | ) 97 | } 98 | } 99 | 100 | @Composable 101 | fun BluromaticScreenContent( 102 | blurUiState: BlurUiState, 103 | blurAmountOptions: List, 104 | applyBlur: (Int) -> Unit, 105 | cancelWork: () -> Unit, 106 | modifier: Modifier = Modifier 107 | ) { 108 | var selectedValue by rememberSaveable { mutableStateOf(1) } 109 | val context = LocalContext.current 110 | Column(modifier = modifier) { 111 | Image( 112 | painter = painterResource(R.drawable.android_cupcake), 113 | contentDescription = stringResource(R.string.description_image), 114 | modifier = Modifier 115 | .fillMaxWidth() 116 | .height(400.dp), 117 | contentScale = ContentScale.Fit, 118 | ) 119 | BlurAmountContent( 120 | selectedValue = selectedValue, 121 | blurAmounts = blurAmountOptions, 122 | modifier = Modifier.fillMaxWidth(), 123 | onSelectedValueChange = { selectedValue = it } 124 | ) 125 | BlurActions( 126 | blurUiState = blurUiState, 127 | onStartClick = { applyBlur(selectedValue) }, 128 | onSeeFileClick = { currentUri -> 129 | showBlurredImage(context, currentUri) 130 | }, 131 | onCancelClick = { cancelWork() }, 132 | modifier = Modifier.fillMaxWidth() 133 | ) 134 | } 135 | } 136 | 137 | @Composable 138 | private fun BlurActions( 139 | blurUiState: BlurUiState, 140 | onStartClick: () -> Unit, 141 | onSeeFileClick: (String) -> Unit, 142 | onCancelClick: () -> Unit, 143 | modifier: Modifier = Modifier 144 | ) { 145 | Row( 146 | modifier = modifier, 147 | horizontalArrangement = Arrangement.Center 148 | ) { 149 | when (blurUiState) { 150 | is BlurUiState.Default -> { 151 | Button( 152 | onClick = onStartClick, 153 | modifier = Modifier.fillMaxWidth() 154 | ) { 155 | Text(stringResource(R.string.start)) 156 | } 157 | } 158 | 159 | is BlurUiState.Loading -> { 160 | FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) } 161 | CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))) 162 | } 163 | 164 | is BlurUiState.Complete -> { 165 | Button(onStartClick) { Text(stringResource(R.string.start)) } 166 | Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_small))) 167 | FilledTonalButton({ onSeeFileClick(blurUiState.outputUri) }) 168 | { Text(stringResource(R.string.see_file)) } 169 | } 170 | } 171 | } 172 | } 173 | 174 | @Composable 175 | private fun BlurAmountContent( 176 | selectedValue: Int, 177 | blurAmounts: List, 178 | modifier: Modifier = Modifier, 179 | onSelectedValueChange: (Int) -> Unit 180 | ) { 181 | Column( 182 | modifier = modifier.selectableGroup() 183 | ) { 184 | Text( 185 | text = stringResource(R.string.blur_title), 186 | style = MaterialTheme.typography.headlineSmall 187 | ) 188 | blurAmounts.forEach { amount -> 189 | Row( 190 | verticalAlignment = Alignment.CenterVertically, 191 | modifier = Modifier 192 | .fillMaxWidth() 193 | .selectable( 194 | role = Role.RadioButton, 195 | selected = (selectedValue == amount.blurAmount), 196 | onClick = { onSelectedValueChange(amount.blurAmount) } 197 | ) 198 | .size(48.dp) 199 | ) { 200 | RadioButton( 201 | selected = (selectedValue == amount.blurAmount), 202 | onClick = null, 203 | modifier = Modifier.size(48.dp), 204 | colors = RadioButtonDefaults.colors( 205 | selectedColor = MaterialTheme.colorScheme.primary 206 | ) 207 | ) 208 | Text(stringResource(amount.blurAmountRes)) 209 | } 210 | } 211 | } 212 | } 213 | 214 | private fun showBlurredImage(context: Context, currentUri: String) { 215 | val uri = if (currentUri.isNotEmpty()) { 216 | Uri.parse(currentUri) 217 | } else { 218 | null 219 | } 220 | val actionView = Intent(Intent.ACTION_VIEW, uri) 221 | context.startActivity(actionView) 222 | } 223 | 224 | @Preview(showBackground = true) 225 | @Composable 226 | fun BluromaticScreenContentPreview() { 227 | BluromaticTheme { 228 | BluromaticScreenContent( 229 | blurUiState = BlurUiState.Default, 230 | blurAmountOptions = listOf(BlurAmount(R.string.blur_lv_1, 1)), 231 | {}, 232 | {}, 233 | modifier = Modifier.padding(16.dp) 234 | ) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.ui.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | val md_theme_light_primary = Color(0xFF006A68) 22 | val md_theme_light_secondaryContainer = Color(0xFFCCE8E6) 23 | val md_theme_light_background = Color(0xFFFAFDFC) 24 | 25 | val md_theme_dark_primary = Color(0xFF2EDCD8) 26 | val md_theme_dark_secondaryContainer = Color(0xFF324B4A) 27 | val md_theme_dark_background = Color(0xFF191C1C) 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.ui.theme 18 | 19 | import android.app.Activity 20 | import android.os.Build 21 | import androidx.compose.foundation.isSystemInDarkTheme 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.darkColorScheme 24 | import androidx.compose.material3.dynamicDarkColorScheme 25 | import androidx.compose.material3.dynamicLightColorScheme 26 | import androidx.compose.material3.lightColorScheme 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.SideEffect 29 | import androidx.compose.ui.graphics.toArgb 30 | import androidx.compose.ui.platform.LocalContext 31 | import androidx.compose.ui.platform.LocalView 32 | import androidx.core.view.WindowCompat 33 | 34 | private val DarkColorScheme = darkColorScheme( 35 | primary = md_theme_dark_primary, 36 | secondaryContainer = md_theme_dark_secondaryContainer, 37 | background = md_theme_dark_background, 38 | ) 39 | 40 | private val LightColorScheme = lightColorScheme( 41 | primary = md_theme_light_primary, 42 | secondaryContainer = md_theme_light_secondaryContainer, 43 | background = md_theme_light_background, 44 | ) 45 | 46 | @Composable 47 | fun BluromaticTheme( 48 | darkTheme: Boolean = isSystemInDarkTheme(), 49 | // Dynamic color is available on Android 12+ 50 | // Dynamic color in this app is turned off for learning purposes 51 | dynamicColor: Boolean = false, 52 | content: @Composable () -> Unit 53 | ) { 54 | val colorScheme = when { 55 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 56 | val context = LocalContext.current 57 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 58 | } 59 | 60 | darkTheme -> DarkColorScheme 61 | else -> LightColorScheme 62 | } 63 | val view = LocalView.current 64 | if (!view.isInEditMode) { 65 | SideEffect { 66 | val window = (view.context as Activity).window 67 | window.statusBarColor = colorScheme.background.toArgb() 68 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 69 | } 70 | } 71 | 72 | MaterialTheme( 73 | colorScheme = colorScheme, 74 | content = content 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/workers/BlurWorker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.workers 18 | 19 | import android.content.Context 20 | import android.graphics.BitmapFactory 21 | import android.net.Uri 22 | import android.util.Log 23 | import androidx.work.CoroutineWorker 24 | import androidx.work.WorkerParameters 25 | import androidx.work.workDataOf 26 | import com.example.bluromatic.DELAY_TIME_MILLIS 27 | import com.example.bluromatic.KEY_BLUR_LEVEL 28 | import com.example.bluromatic.KEY_IMAGE_URI 29 | import com.example.bluromatic.R 30 | import kotlinx.coroutines.Dispatchers 31 | import kotlinx.coroutines.delay 32 | import kotlinx.coroutines.withContext 33 | 34 | private const val TAG = "BlurWorker" 35 | 36 | class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { 37 | 38 | override suspend fun doWork(): Result { 39 | val resourceUri = inputData.getString(KEY_IMAGE_URI) 40 | val blurLevel = inputData.getInt(KEY_BLUR_LEVEL, 1) 41 | 42 | makeStatusNotification( 43 | applicationContext.resources.getString(R.string.blurring_image), 44 | applicationContext 45 | ) 46 | 47 | return withContext(Dispatchers.IO) { 48 | 49 | // This is an utility function added to emulate slower work. 50 | delay(DELAY_TIME_MILLIS) 51 | 52 | return@withContext try { 53 | require(!resourceUri.isNullOrBlank()) { 54 | val errorMessage = 55 | applicationContext.resources.getString(R.string.invalid_input_uri) 56 | Log.e(TAG, errorMessage) 57 | errorMessage 58 | } 59 | val resolver = applicationContext.contentResolver 60 | 61 | val picture = BitmapFactory.decodeStream( 62 | resolver.openInputStream(Uri.parse(resourceUri)) 63 | ) 64 | 65 | val output = blurBitmap(picture, blurLevel) 66 | 67 | // Write bitmap to a temp file 68 | val outputUri = writeBitmapToFile(applicationContext, output) 69 | 70 | val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString()) 71 | 72 | Result.success(outputData) 73 | } catch (throwable: Throwable) { 74 | Log.e( 75 | TAG, 76 | applicationContext.resources.getString(R.string.error_applying_blur), 77 | throwable 78 | ) 79 | Result.failure() 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/workers/CleanupWorker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.workers 18 | 19 | import android.content.Context 20 | import android.util.Log 21 | import androidx.work.CoroutineWorker 22 | import androidx.work.WorkerParameters 23 | import com.example.bluromatic.DELAY_TIME_MILLIS 24 | import com.example.bluromatic.OUTPUT_PATH 25 | import com.example.bluromatic.R 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.delay 28 | import kotlinx.coroutines.withContext 29 | import java.io.File 30 | 31 | /** 32 | * Cleans up temporary files generated during blurring process 33 | */ 34 | private const val TAG = "CleanupWorker" 35 | 36 | class CleanupWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { 37 | 38 | override suspend fun doWork(): Result { 39 | /** Makes a notification when the work starts and slows down the work so that it's easier 40 | * to see each WorkRequest start, even on emulated devices 41 | */ 42 | makeStatusNotification( 43 | applicationContext.resources.getString(R.string.cleaning_up_files), 44 | applicationContext 45 | ) 46 | 47 | return withContext(Dispatchers.IO) { 48 | delay(DELAY_TIME_MILLIS) 49 | 50 | return@withContext try { 51 | val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH) 52 | if (outputDirectory.exists()) { 53 | val entries = outputDirectory.listFiles() 54 | if (entries != null) { 55 | for (entry in entries) { 56 | val name = entry.name 57 | if (name.isNotEmpty() && name.endsWith(".png")) { 58 | val deleted = entry.delete() 59 | Log.i(TAG, "Deleted $name - $deleted") 60 | } 61 | } 62 | } 63 | } 64 | Result.success() 65 | } catch (exception: Exception) { 66 | Log.e( 67 | TAG, 68 | applicationContext.resources.getString(R.string.error_cleaning_file), 69 | exception 70 | ) 71 | Result.failure() 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/workers/SaveImageToFileWorker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.workers 18 | 19 | import android.content.Context 20 | import android.graphics.BitmapFactory 21 | import android.net.Uri 22 | import android.os.Build 23 | import android.provider.MediaStore 24 | import android.util.Log 25 | import androidx.work.CoroutineWorker 26 | import androidx.work.WorkerParameters 27 | import androidx.work.workDataOf 28 | import com.example.bluromatic.DELAY_TIME_MILLIS 29 | import com.example.bluromatic.KEY_IMAGE_URI 30 | import com.example.bluromatic.R 31 | import com.example.bluromatic.domain.CreateImageUriUseCase 32 | import com.example.bluromatic.domain.SaveImageUseCase 33 | import kotlinx.coroutines.Dispatchers 34 | import kotlinx.coroutines.delay 35 | import kotlinx.coroutines.withContext 36 | import java.text.SimpleDateFormat 37 | import java.util.Date 38 | import java.util.Locale 39 | 40 | /** 41 | * Saves the image to a permanent file 42 | */ 43 | private const val TAG = "SaveImageToFileWorker" 44 | 45 | class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { 46 | 47 | private val title = "Blurred Image" 48 | private val dateFormatter = SimpleDateFormat( 49 | "yyyy.MM.dd 'at' HH:mm:ss z", 50 | Locale.getDefault() 51 | ) 52 | 53 | override suspend fun doWork(): Result { 54 | // Makes a notification when the work starts and slows down the work so that 55 | // it's easier to see each WorkRequest start, even on emulated devices 56 | makeStatusNotification( 57 | applicationContext.resources.getString(R.string.saving_image), 58 | applicationContext 59 | ) 60 | 61 | return withContext(Dispatchers.IO) { 62 | delay(DELAY_TIME_MILLIS) 63 | 64 | val resolver = applicationContext.contentResolver 65 | return@withContext try { 66 | val resourceUri = inputData.getString(KEY_IMAGE_URI) 67 | val bitmap = BitmapFactory.decodeStream( 68 | resolver.openInputStream(Uri.parse(resourceUri)) 69 | ) 70 | 71 | val imageUrl: String? 72 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 73 | val createImageUriUseCase = CreateImageUriUseCase() 74 | val saveImageUseCase = SaveImageUseCase() 75 | 76 | val imageUri = createImageUriUseCase( 77 | resolver, 78 | "${title}_${dateFormatter.format(Date())}.jpg" 79 | ) 80 | 81 | saveImageUseCase(resolver, imageUri, bitmap) 82 | imageUrl = imageUri.toString() 83 | } else { 84 | @Suppress("DEPRECATION") 85 | imageUrl = MediaStore.Images.Media.insertImage( 86 | resolver, bitmap, title, dateFormatter.format(Date()) 87 | ) 88 | } 89 | if (!imageUrl.isNullOrEmpty()) { 90 | val output = workDataOf(KEY_IMAGE_URI to imageUrl) 91 | 92 | Result.success(output) 93 | } else { 94 | Log.e( 95 | TAG, 96 | applicationContext.resources.getString( 97 | R.string.writing_to_mediaStore_failed 98 | ) 99 | ) 100 | Result.failure() 101 | } 102 | } catch (exception: Exception) { 103 | Log.e( 104 | TAG, 105 | applicationContext.resources.getString(R.string.error_saving_image), 106 | exception 107 | ) 108 | Result.failure() 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/bluromatic/workers/WorkerUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.bluromatic.workers 18 | 19 | import android.app.NotificationChannel 20 | import android.app.NotificationManager 21 | import android.content.Context 22 | import android.graphics.Bitmap 23 | import android.net.Uri 24 | import android.os.Build 25 | import android.util.Log 26 | import androidx.annotation.WorkerThread 27 | import androidx.core.app.NotificationCompat 28 | import androidx.core.app.NotificationManagerCompat 29 | import com.example.bluromatic.CHANNEL_ID 30 | import com.example.bluromatic.NOTIFICATION_ID 31 | import com.example.bluromatic.NOTIFICATION_TITLE 32 | import com.example.bluromatic.OUTPUT_PATH 33 | import com.example.bluromatic.R 34 | import com.example.bluromatic.VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION 35 | import com.example.bluromatic.VERBOSE_NOTIFICATION_CHANNEL_NAME 36 | import java.io.File 37 | import java.io.FileNotFoundException 38 | import java.io.FileOutputStream 39 | import java.io.IOException 40 | import java.util.UUID 41 | 42 | private const val TAG = "WorkerUtils" 43 | 44 | /** 45 | * Create a Notification that is shown as a heads-up notification if possible. 46 | * 47 | * For this codelab, this is used to show a notification so that you know when different steps 48 | * of the background work chain are starting 49 | * 50 | * @param message Message shown on the notification 51 | * @param context Context needed to create Toast 52 | */ 53 | fun makeStatusNotification(message: String, context: Context) { 54 | 55 | // Make a channel if necessary 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 57 | // Create the NotificationChannel, but only on API 26+ because 58 | // the NotificationChannel class is new and not in the support library 59 | val name = VERBOSE_NOTIFICATION_CHANNEL_NAME 60 | val description = VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION 61 | val importance = NotificationManager.IMPORTANCE_HIGH 62 | val channel = NotificationChannel(CHANNEL_ID, name, importance) 63 | channel.description = description 64 | 65 | // Add the channel 66 | val notificationManager = 67 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? 68 | 69 | notificationManager?.createNotificationChannel(channel) 70 | } 71 | 72 | // Create the notification 73 | val builder = NotificationCompat.Builder(context, CHANNEL_ID) 74 | .setSmallIcon(R.drawable.ic_launcher_foreground) 75 | .setContentTitle(NOTIFICATION_TITLE) 76 | .setContentText(message) 77 | .setPriority(NotificationCompat.PRIORITY_HIGH) 78 | .setVibrate(LongArray(0)) 79 | 80 | // Show the notification 81 | NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build()) 82 | } 83 | 84 | /** 85 | * Blurs the given Bitmap image 86 | * @param bitmap Image to blur 87 | * @param blurLevel Blur level input 88 | * @return Blurred bitmap image 89 | */ 90 | @WorkerThread 91 | fun blurBitmap(bitmap: Bitmap, blurLevel: Int): Bitmap { 92 | val input = Bitmap.createScaledBitmap( 93 | bitmap, 94 | bitmap.width/(blurLevel*5), 95 | bitmap.height/(blurLevel*5), 96 | true 97 | ) 98 | return Bitmap.createScaledBitmap(input, bitmap.width, bitmap.height, true) 99 | } 100 | 101 | /** 102 | * Writes bitmap to a temporary file and returns the Uri for the file 103 | * @param applicationContext Application context 104 | * @param bitmap Bitmap to write to temp file 105 | * @return Uri for temp file with bitmap 106 | * @throws FileNotFoundException Throws if bitmap file cannot be found 107 | */ 108 | @Throws(FileNotFoundException::class) 109 | fun writeBitmapToFile(applicationContext: Context, bitmap: Bitmap): Uri { 110 | val name = String.format("blur-filter-output-%s.png", UUID.randomUUID().toString()) 111 | val outputDir = File(applicationContext.filesDir, OUTPUT_PATH) 112 | if (!outputDir.exists()) { 113 | outputDir.mkdirs() // should succeed 114 | } 115 | val outputFile = File(outputDir, name) 116 | var out: FileOutputStream? = null 117 | try { 118 | out = FileOutputStream(outputFile) 119 | bitmap.compress(Bitmap.CompressFormat.PNG, 0 /* ignored for PNG */, out) 120 | } finally { 121 | out?.let { 122 | try { 123 | it.close() 124 | } catch (e: IOException) { 125 | Log.e(TAG, e.message.toString()) 126 | } 127 | } 128 | } 129 | return Uri.fromFile(outputFile) 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/android_cupcake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/drawable-nodpi/android_cupcake.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 25 | 31 | 34 | 37 | 38 | 39 | 40 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 176 | 181 | 186 | 187 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-developer-training/basic-android-kotlin-compose-training-workmanager/91447deb381a65bfcb880ffbf0b80ea0c410a318/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 8.dp 20 | 16.dp 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Blur-O-Matic 19 | 20 | 21 | Select Blur Amount 22 | 23 | 24 | Select Image 25 | Start 26 | See File 27 | 28 | 29 | The image to blur and save. 30 | 31 | 32 | A little blurred 33 | More blurred 34 | The most blurred 35 | Cancel Work 36 | 37 | 38 | Go to Settings -> Apps and Notifications -> 39 | WorkManager Demo -> App Permissions and grant access to Storage. 40 | 41 | 42 | Blurring image 43 | Saving image 44 | Cleaning up old temporary files 45 | 46 | 47 | Invalid input uri 48 | Error applying blur 49 | Writing to MediaStore failed 50 | Error saving image 51 | Error cleaning up old temporary files 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 |