├── CONTRIBUTING.md ├── LICENSE ├── PhotoLog_End ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── com.example.photolog_end.AppDatabase │ │ └── 1.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── photolog_end │ │ ├── AddLogScreen.kt │ │ ├── AddLogViewModel.kt │ │ ├── AppDatabase.kt │ │ ├── CameraScreen.kt │ │ ├── CameraViewModel.kt │ │ ├── HomeScreen.kt │ │ ├── HomeViewModel.kt │ │ ├── Log.kt │ │ ├── LogDao.kt │ │ ├── LogEntry.kt │ │ ├── MainActivity.kt │ │ ├── MediaRepository.kt │ │ ├── PermissionManager.kt │ │ ├── PermissionScreen.kt │ │ ├── PermissionViewModel.kt │ │ ├── PhotoGoodApplication.kt │ │ ├── PhotoGrid.kt │ │ ├── PhotoSaverRepository.kt │ │ └── ui │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── 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 │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── PhotoLog_start ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── com.example.photolog_start.AppDatabase │ │ └── 1.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── photolog_start │ │ ├── AddLogScreen.kt │ │ ├── AddLogViewModel.kt │ │ ├── AppDatabase.kt │ │ ├── CameraScreen.kt │ │ ├── CameraViewModel.kt │ │ ├── HomeScreen.kt │ │ ├── HomeViewModel.kt │ │ ├── Log.kt │ │ ├── LogDao.kt │ │ ├── LogEntry.kt │ │ ├── MainActivity.kt │ │ ├── MediaRepository.kt │ │ ├── PermissionManager.kt │ │ ├── PermissionScreen.kt │ │ ├── PermissionViewModel.kt │ │ ├── PhotoGrid.kt │ │ ├── PhotoLogApplication.kt │ │ ├── PhotoSaverRepository.kt │ │ └── ui │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── 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 │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 30 | -------------------------------------------------------------------------------- /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. 203 | -------------------------------------------------------------------------------- /PhotoLog_End/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /PhotoLog_End/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.kapt" 21 | } 22 | 23 | android { 24 | compileSdk 33 25 | 26 | defaultConfig { 27 | applicationId "com.example.photogood" 28 | minSdk 21 29 | targetSdk 33 30 | versionCode 1 31 | versionName "1.0" 32 | 33 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 34 | vectorDrawables { 35 | useSupportLibrary true 36 | } 37 | 38 | javaCompileOptions { 39 | annotationProcessorOptions { 40 | arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] 41 | } 42 | } 43 | } 44 | 45 | buildTypes { 46 | release { 47 | minifyEnabled false 48 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 49 | } 50 | } 51 | compileOptions { 52 | sourceCompatibility JavaVersion.VERSION_1_8 53 | targetCompatibility JavaVersion.VERSION_1_8 54 | } 55 | kotlinOptions { 56 | jvmTarget = '1.8' 57 | freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=enable"] 58 | } 59 | buildFeatures { 60 | compose true 61 | } 62 | composeOptions { 63 | kotlinCompilerExtensionVersion compose_version 64 | } 65 | packagingOptions { 66 | resources { 67 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 68 | } 69 | } 70 | } 71 | 72 | dependencies { 73 | 74 | implementation 'androidx.core:core-ktx:1.8.0' 75 | implementation "androidx.compose.ui:ui:$compose_version" 76 | implementation 'androidx.compose.material3:material3:1.0.0-alpha14' 77 | implementation "androidx.compose.material:material:$compose_version" 78 | implementation "androidx.compose.material:material-icons-extended:$compose_version" 79 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 80 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0' 81 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0' 82 | implementation 'androidx.activity:activity-compose:1.5.0' 83 | implementation 'androidx.activity:activity-ktx:1.6.0-alpha05' 84 | 85 | implementation "androidx.navigation:navigation-compose:2.5.0" 86 | implementation "io.coil-kt:coil-compose:2.1.0" 87 | implementation 'androidx.appcompat:appcompat:1.3.0' 88 | implementation 'com.google.android.material:material:1.4.0' 89 | 90 | def room_version = "2.4.2" 91 | implementation "androidx.room:room-runtime:$room_version" 92 | kapt "androidx.room:room-compiler:$room_version" 93 | implementation "androidx.room:room-ktx:$room_version" 94 | 95 | implementation 'com.google.android.gms:play-services-location:20.0.0' 96 | 97 | implementation("androidx.camera:camera-camera2:1.2.0-alpha02") 98 | implementation("androidx.camera:camera-lifecycle:1.2.0-alpha02") 99 | implementation("androidx.camera:camera-view:1.2.0-alpha02") 100 | 101 | testImplementation 'junit:junit:4.13.2' 102 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 103 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 104 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 105 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 106 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" 107 | } -------------------------------------------------------------------------------- /PhotoLog_End/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 -------------------------------------------------------------------------------- /PhotoLog_End/schemas/com.example.photolog_end.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "bc383dc1a65f0f5713eeb2fb5b057f62", 6 | "entities": [ 7 | { 8 | "tableName": "logs", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, `place` TEXT NOT NULL, `photo1_name` TEXT NOT NULL, `photo2_name` TEXT, `photo3_name` TEXT, PRIMARY KEY(`date`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "date", 13 | "columnName": "date", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "place", 19 | "columnName": "place", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "photo1", 25 | "columnName": "photo1_name", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "photo2", 31 | "columnName": "photo2_name", 32 | "affinity": "TEXT", 33 | "notNull": false 34 | }, 35 | { 36 | "fieldPath": "photo3", 37 | "columnName": "photo3_name", 38 | "affinity": "TEXT", 39 | "notNull": false 40 | } 41 | ], 42 | "primaryKey": { 43 | "columnNames": [ 44 | "date" 45 | ], 46 | "autoGenerate": false 47 | }, 48 | "indices": [], 49 | "foreignKeys": [] 50 | } 51 | ], 52 | "views": [], 53 | "setupQueries": [ 54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc383dc1a65f0f5713eeb2fb5b057f62')" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/AddLogViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.Manifest 20 | import android.annotation.SuppressLint 21 | import android.app.Application 22 | import android.content.Context 23 | import android.content.Intent 24 | import android.content.pm.PackageManager 25 | import android.location.Geocoder 26 | import android.net.Uri 27 | import android.os.Build 28 | import android.provider.Settings 29 | import android.util.Log 30 | import androidx.compose.runtime.getValue 31 | import androidx.compose.runtime.mutableStateOf 32 | import androidx.compose.runtime.setValue 33 | import androidx.core.content.ContextCompat 34 | import androidx.lifecycle.AndroidViewModel 35 | import androidx.lifecycle.ViewModel 36 | import androidx.lifecycle.ViewModelProvider 37 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 38 | import androidx.lifecycle.viewModelScope 39 | import androidx.lifecycle.viewmodel.CreationExtras 40 | import androidx.room.Room 41 | import com.example.photolog_end.AppDatabase.Companion.DB_NAME 42 | import com.google.android.gms.location.LocationServices 43 | import kotlinx.coroutines.flow.map 44 | import kotlinx.coroutines.flow.toList 45 | import kotlinx.coroutines.launch 46 | import java.io.File 47 | import java.text.SimpleDateFormat 48 | import java.util.Calendar 49 | import java.util.Locale 50 | 51 | class AddLogViewModel( 52 | application: Application, 53 | private val photoSaver: PhotoSaverRepository 54 | ) : AndroidViewModel(application) { 55 | // region ViewModel setup 56 | private val context: Context 57 | get() = getApplication() 58 | 59 | private val mediaRepository = MediaRepository(context) 60 | private val db = Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME).build() 61 | // endregion 62 | 63 | // region UI state 64 | data class UiState( 65 | val hasLocationAccess: Boolean, 66 | val hasCameraAccess: Boolean, 67 | val isSaving: Boolean = false, 68 | val isSaved: Boolean = false, 69 | val date: Long, 70 | val place: String? = null, 71 | val savedPhotos: List = emptyList(), 72 | val localPickerPhotos: List = emptyList() 73 | ) 74 | 75 | var uiState by mutableStateOf( 76 | UiState( 77 | hasLocationAccess = hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION), 78 | hasCameraAccess = hasPermission(Manifest.permission.CAMERA), 79 | date = getTodayDateInMillis(), 80 | savedPhotos = photoSaver.getPhotos() 81 | ) 82 | ) 83 | private set 84 | 85 | fun isValid(): Boolean { 86 | return uiState.place != null && !photoSaver.isEmpty() && !uiState.isSaving 87 | } 88 | 89 | private fun getTodayDateInMillis(): Long { 90 | val calendar = Calendar.getInstance() 91 | calendar.set(Calendar.HOUR, 0) 92 | calendar.set(Calendar.MINUTE, 0) 93 | calendar.set(Calendar.SECOND, 0) 94 | return calendar.timeInMillis 95 | } 96 | 97 | private fun getIsoDate(timeInMillis: Long): String { 98 | return SimpleDateFormat("yyyy-MM-dd", Locale.US).format(timeInMillis) 99 | } 100 | 101 | fun hasPermission(permission: String): Boolean { 102 | return ContextCompat.checkSelfPermission( 103 | context, 104 | permission 105 | ) == PackageManager.PERMISSION_GRANTED 106 | } 107 | 108 | fun onPermissionChange(permission: String, isGranted: Boolean) { 109 | when (permission) { 110 | Manifest.permission.ACCESS_COARSE_LOCATION -> { 111 | uiState = uiState.copy(hasLocationAccess = isGranted) 112 | } 113 | Manifest.permission.CAMERA -> { 114 | uiState = uiState.copy(hasCameraAccess = isGranted) 115 | } 116 | else -> { 117 | Log.e("Permission change", "Unexpected permission: $permission") 118 | } 119 | } 120 | } 121 | 122 | fun onDateChange(dateInMillis: Long) { 123 | uiState = uiState.copy(date = dateInMillis) 124 | } 125 | 126 | fun createSettingsIntent(): Intent { 127 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 128 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 129 | data = Uri.fromParts("package", context.packageName, null) 130 | } 131 | 132 | return intent 133 | } 134 | // endregion 135 | 136 | // region Location management 137 | @SuppressLint("MissingPermission") 138 | fun fetchLocation() { 139 | val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) 140 | fusedLocationClient.lastLocation.addOnSuccessListener { location -> 141 | location ?: return@addOnSuccessListener 142 | 143 | val geocoder = Geocoder(context, Locale.getDefault()) 144 | 145 | if (Build.VERSION.SDK_INT >= 33) 146 | { 147 | geocoder.getFromLocation(location.latitude, location.longitude, 1) { addresses -> 148 | val address = addresses.firstOrNull() 149 | val place = address?.locality ?: address?.subAdminArea ?: address?.adminArea 150 | ?: address?.countryName 151 | uiState = uiState.copy(place = place) 152 | } 153 | } 154 | else { 155 | val address = 156 | geocoder.getFromLocation(location.latitude, location.longitude, 1)?.firstOrNull() 157 | ?: return@addOnSuccessListener 158 | val place = 159 | address.locality ?: address.subAdminArea ?: address.adminArea ?: address.countryName 160 | ?: return@addOnSuccessListener 161 | 162 | uiState = uiState.copy(place = place) 163 | } 164 | } 165 | } 166 | // endregion 167 | 168 | fun loadLocalPickerPictures() { 169 | viewModelScope.launch { 170 | val localPickerPhotos = mediaRepository.fetchImages().map { it.uri }.toList() 171 | uiState = uiState.copy(localPickerPhotos = localPickerPhotos) 172 | } 173 | } 174 | 175 | fun onLocalPhotoPickerSelect(photo: Uri) { 176 | viewModelScope.launch { 177 | photoSaver.cacheFromUri(photo) 178 | refreshSavedPhotos() 179 | } 180 | } 181 | 182 | fun onPhotoPickerSelect(photos: List) { 183 | viewModelScope.launch { 184 | photoSaver.cacheFromUris(photos) 185 | refreshSavedPhotos() 186 | } 187 | } 188 | 189 | // region Photo management 190 | 191 | fun canAddPhoto() = photoSaver.canAddPhoto() 192 | 193 | fun refreshSavedPhotos() { 194 | uiState = uiState.copy(savedPhotos = photoSaver.getPhotos()) 195 | } 196 | 197 | fun onPhotoRemoved(photo: File) { 198 | viewModelScope.launch { 199 | photoSaver.removeFile(photo) 200 | refreshSavedPhotos() 201 | } 202 | } 203 | 204 | fun createLog() { 205 | if (!isValid()) { 206 | return 207 | } 208 | 209 | uiState = uiState.copy(isSaving = true) 210 | 211 | viewModelScope.launch { 212 | val photos = photoSaver.savePhotos() 213 | 214 | val calendar = Calendar.getInstance() 215 | calendar.timeInMillis = uiState.date 216 | Log.e("date is ", uiState.date.toString()) 217 | 218 | val log = LogEntry( 219 | date = getIsoDate(uiState.date), 220 | place = uiState.place!!, 221 | photo1 = photos[0].name, 222 | photo2 = photos.getOrNull(1)?.name, 223 | photo3 = photos.getOrNull(2)?.name, 224 | ) 225 | 226 | db.logDao().insert(log) 227 | uiState = uiState.copy(isSaved = true) 228 | } 229 | } 230 | // endregion 231 | } 232 | 233 | class AddLogViewModelFactory : ViewModelProvider.Factory { 234 | @Suppress("UNCHECKED_CAST") 235 | override fun create(modelClass: Class, extras: CreationExtras): T { 236 | val app = extras[APPLICATION_KEY] as PhotoGoodApplication 237 | return AddLogViewModel(app, app.photoSaver) as T 238 | } 239 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import androidx.room.Database 20 | import androidx.room.RoomDatabase 21 | 22 | @Database(entities = [LogEntry::class], version = 1) 23 | abstract class AppDatabase : RoomDatabase() { 24 | companion object { 25 | const val DB_NAME = "main" 26 | } 27 | 28 | abstract fun logDao(): LogDao 29 | } 30 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/CameraScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.util.Log 20 | import android.view.ViewGroup 21 | import android.view.ViewGroup.LayoutParams.MATCH_PARENT 22 | import androidx.camera.core.CameraSelector 23 | import androidx.camera.core.Preview 24 | import androidx.camera.view.PreviewView 25 | import androidx.compose.foundation.layout.fillMaxSize 26 | import androidx.compose.foundation.layout.padding 27 | import androidx.compose.material.icons.Icons 28 | import androidx.compose.material.icons.filled.PhotoCamera 29 | import androidx.compose.material3.BottomAppBarDefaults 30 | import androidx.compose.material3.ExperimentalMaterial3Api 31 | import androidx.compose.material3.Icon 32 | import androidx.compose.material3.Scaffold 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.runtime.LaunchedEffect 35 | import androidx.compose.runtime.remember 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.platform.LocalLifecycleOwner 38 | import androidx.compose.ui.viewinterop.AndroidView 39 | import androidx.lifecycle.viewmodel.compose.viewModel 40 | import androidx.navigation.NavHostController 41 | 42 | @OptIn(ExperimentalMaterial3Api::class) 43 | @Composable 44 | fun CameraScreen( 45 | navController: NavHostController, 46 | viewModel: CameraViewModel = viewModel(factory = CameraViewModelFactory()) 47 | ) { 48 | val lifecycleOwner = LocalLifecycleOwner.current 49 | val state = viewModel.cameraState 50 | 51 | val previewUseCase = remember { Preview.Builder().build() } 52 | 53 | LaunchedEffect(Unit) { 54 | val cameraProvider = viewModel.getCameraProvider() 55 | try { 56 | cameraProvider.unbindAll() 57 | cameraProvider.bindToLifecycle( 58 | lifecycleOwner, 59 | CameraSelector.DEFAULT_BACK_CAMERA, 60 | previewUseCase, 61 | state.imageCapture 62 | ) 63 | } catch (ex: Exception) { 64 | Log.e("CameraCapture", "Failed to bind camera use cases", ex) 65 | } 66 | } 67 | 68 | LaunchedEffect(state.imageFile) { 69 | if (state.imageFile != null) { 70 | navController.popBackStack() 71 | } 72 | } 73 | 74 | Scaffold( 75 | floatingActionButton = { 76 | BottomAppBarDefaults.FloatingActionButton( 77 | onClick = { if (!state.isTakingPicture) viewModel.takePicture() } 78 | ) { 79 | Icon(Icons.Filled.PhotoCamera, contentDescription = "Take picture") 80 | } 81 | } 82 | ) { innerPadding -> 83 | AndroidView( 84 | modifier = Modifier 85 | .padding(innerPadding) 86 | .fillMaxSize(), 87 | factory = { context -> 88 | PreviewView(context).apply { 89 | layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) 90 | 91 | previewUseCase.setSurfaceProvider(this.surfaceProvider) 92 | } 93 | } 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/CameraViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.app.Application 20 | import android.content.Context 21 | import android.util.Log 22 | import androidx.camera.core.ImageCapture 23 | import androidx.camera.core.ImageCaptureException 24 | import androidx.camera.lifecycle.ProcessCameraProvider 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.setValue 28 | import androidx.lifecycle.AndroidViewModel 29 | import androidx.lifecycle.ViewModel 30 | import androidx.lifecycle.ViewModelProvider 31 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 32 | import androidx.lifecycle.viewModelScope 33 | import androidx.lifecycle.viewmodel.CreationExtras 34 | import kotlinx.coroutines.launch 35 | import java.io.File 36 | import java.util.concurrent.Executors 37 | import kotlin.coroutines.resume 38 | import kotlin.coroutines.suspendCoroutine 39 | 40 | class CameraViewModel( 41 | application: Application, 42 | private val photoSaver: PhotoSaverRepository 43 | ) : AndroidViewModel(application) { 44 | 45 | private val context: Context 46 | get() = getApplication() 47 | private val cameraExecutor = Executors.newSingleThreadExecutor() 48 | 49 | data class CameraState( 50 | val isTakingPicture: Boolean = false, 51 | val imageCapture: ImageCapture = ImageCapture.Builder() 52 | .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).build(), 53 | val imageFile: File? = null, 54 | val captureError: ImageCaptureException? = null 55 | ) 56 | 57 | var cameraState by mutableStateOf(CameraState()) 58 | private set 59 | 60 | suspend fun getCameraProvider(): ProcessCameraProvider { 61 | return suspendCoroutine { continuation -> 62 | ProcessCameraProvider.getInstance(context).apply { 63 | addListener({ continuation.resume(get()) }, cameraExecutor) 64 | } 65 | } 66 | } 67 | 68 | fun takePicture() { 69 | viewModelScope.launch { 70 | cameraState = cameraState.copy(isTakingPicture = true) 71 | 72 | val savedFile = photoSaver.generatePhotoCacheFile() 73 | 74 | cameraState.imageCapture.takePicture( 75 | ImageCapture.OutputFileOptions.Builder(savedFile).build(), 76 | cameraExecutor, 77 | object : ImageCapture.OnImageSavedCallback { 78 | override fun onImageSaved(output: ImageCapture.OutputFileResults) { 79 | Log.i("TakePicture", "capture succeeded") 80 | 81 | photoSaver.cacheCapturedPhoto(savedFile) 82 | cameraState = cameraState.copy(imageFile = savedFile) 83 | } 84 | 85 | override fun onError(ex: ImageCaptureException) { 86 | Log.e("TakePicture", "capture failed", ex) 87 | } 88 | } 89 | ) 90 | } 91 | } 92 | } 93 | 94 | class CameraViewModelFactory : ViewModelProvider.Factory { 95 | @Suppress("UNCHECKED_CAST") 96 | override fun create(modelClass: Class, extras: CreationExtras): T { 97 | val app = extras[APPLICATION_KEY] as PhotoGoodApplication 98 | return CameraViewModel(app, app.photoSaver) as T 99 | } 100 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import androidx.compose.foundation.ExperimentalFoundationApi 20 | import androidx.compose.foundation.layout.Arrangement 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.PaddingValues 23 | import androidx.compose.foundation.layout.Row 24 | import androidx.compose.foundation.layout.Spacer 25 | import androidx.compose.foundation.layout.fillMaxSize 26 | import androidx.compose.foundation.layout.fillMaxWidth 27 | import androidx.compose.foundation.layout.height 28 | import androidx.compose.foundation.layout.padding 29 | import androidx.compose.foundation.layout.size 30 | import androidx.compose.foundation.lazy.LazyColumn 31 | import androidx.compose.foundation.lazy.items 32 | import androidx.compose.material.icons.Icons 33 | import androidx.compose.material.icons.filled.Add 34 | import androidx.compose.material.icons.filled.Delete 35 | import androidx.compose.material.icons.filled.Explore 36 | import androidx.compose.material3.ButtonDefaults 37 | import androidx.compose.material3.Card 38 | import androidx.compose.material3.ExperimentalMaterial3Api 39 | import androidx.compose.material3.FloatingActionButton 40 | import androidx.compose.material3.Icon 41 | import androidx.compose.material3.IconButton 42 | import androidx.compose.material3.MaterialTheme 43 | import androidx.compose.material3.Scaffold 44 | import androidx.compose.material3.SmallTopAppBar 45 | import androidx.compose.material3.Text 46 | import androidx.compose.material3.TopAppBarDefaults 47 | import androidx.compose.material3.rememberTopAppBarScrollState 48 | import androidx.compose.runtime.Composable 49 | import androidx.compose.runtime.LaunchedEffect 50 | import androidx.compose.ui.Alignment 51 | import androidx.compose.ui.Modifier 52 | import androidx.compose.ui.input.nestedscroll.nestedScroll 53 | import androidx.compose.ui.text.font.FontFamily 54 | import androidx.compose.ui.unit.dp 55 | import androidx.lifecycle.viewmodel.compose.viewModel 56 | import androidx.navigation.NavHostController 57 | 58 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) 59 | @Composable 60 | fun HomeScreen( 61 | navController: NavHostController, 62 | viewModel: HomeViewModel = viewModel(factory = HomeViewModelFactory()) 63 | ) { 64 | 65 | LaunchedEffect(Unit) { viewModel.loadLogs() } 66 | 67 | val state = viewModel.uiState 68 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarScrollState()) 69 | 70 | Scaffold( 71 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), 72 | topBar = { 73 | SmallTopAppBar( 74 | title = { Text("My Logs", fontFamily = FontFamily.Serif) }, 75 | scrollBehavior = scrollBehavior 76 | ) 77 | }, 78 | floatingActionButton = { 79 | FloatingActionButton(onClick = { navController.navigate(Screens.AddLog.route) }) { 80 | Icon(Icons.Filled.Add, "Add log") 81 | } 82 | } 83 | ) { innerPadding -> 84 | LazyColumn( 85 | Modifier 86 | .fillMaxSize() 87 | .padding(innerPadding), 88 | contentPadding = PaddingValues(8.dp) 89 | ) { 90 | if (!state.loading && state.logs.isEmpty()) { 91 | item { 92 | EmptyLogMessage(Modifier.fillParentMaxSize()) 93 | } 94 | } 95 | items(state.logs, key = { it.date }) { log -> 96 | LogCard( 97 | modifier = Modifier 98 | .fillMaxWidth() 99 | .animateItemPlacement(), 100 | log = log, 101 | formattedDate = viewModel.formatDateTime(log.timeInMillis), 102 | onDelete = viewModel::delete 103 | ) 104 | Spacer(Modifier.height(16.dp)) 105 | } 106 | } 107 | } 108 | } 109 | 110 | @Composable 111 | fun EmptyLogMessage(modifier: Modifier) { 112 | Column( 113 | modifier, 114 | horizontalAlignment = Alignment.CenterHorizontally, 115 | verticalArrangement = Arrangement.Center 116 | ) { 117 | Text( 118 | "Hi there \uD83D\uDC4B", 119 | style = MaterialTheme.typography.headlineMedium, 120 | fontFamily = FontFamily.Serif 121 | ) 122 | Spacer(Modifier.height(16.dp)) 123 | Text( 124 | "Create a log by clicking the ✚ icon below \uD83D\uDC47", 125 | style = MaterialTheme.typography.bodyLarge 126 | ) 127 | } 128 | } 129 | 130 | @OptIn(ExperimentalMaterial3Api::class) 131 | @Composable 132 | fun LogCard(modifier: Modifier, log: Log, formattedDate: String, onDelete: (log: Log) -> Unit) { 133 | Card(modifier) { 134 | Row(Modifier.padding(8.dp, 0.dp), verticalAlignment = Alignment.CenterVertically) { 135 | Text( 136 | text = formattedDate, 137 | modifier = Modifier.weight(1f), 138 | style = MaterialTheme.typography.headlineSmall 139 | ) 140 | IconButton(onClick = { onDelete(log) }) { 141 | Icon( 142 | imageVector = Icons.Filled.Delete, 143 | contentDescription = "Delete log" 144 | ) 145 | } 146 | } 147 | Row(Modifier.padding(8.dp, 0.dp), verticalAlignment = Alignment.CenterVertically) { 148 | Icon(Icons.Filled.Explore, null) 149 | Spacer(Modifier.size(ButtonDefaults.IconSpacing)) 150 | Text(log.place) 151 | } 152 | PhotoGrid(Modifier.padding(16.dp), photos = log.photos) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.app.Application 20 | import android.content.Context 21 | import android.text.format.DateUtils 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.setValue 25 | import androidx.lifecycle.AndroidViewModel 26 | import androidx.lifecycle.ViewModel 27 | import androidx.lifecycle.ViewModelProvider 28 | import androidx.lifecycle.viewModelScope 29 | import androidx.lifecycle.viewmodel.CreationExtras 30 | import androidx.room.Room 31 | import com.example.photolog_end.AppDatabase.Companion.DB_NAME 32 | import kotlinx.coroutines.launch 33 | 34 | class HomeViewModel( 35 | application: Application, 36 | private val photoSaver: PhotoSaverRepository 37 | ) : AndroidViewModel(application) { 38 | 39 | private val context: Context 40 | get() = getApplication() 41 | 42 | private val db = Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME).build() 43 | 44 | data class UiState(val loading: Boolean = true, val logs: List = emptyList()) 45 | 46 | var uiState by mutableStateOf(UiState()) 47 | private set 48 | 49 | fun formatDateTime(timeInMillis: Long): String { 50 | return DateUtils.formatDateTime(context, timeInMillis, DateUtils.FORMAT_ABBREV_ALL) 51 | } 52 | 53 | fun loadLogs() { 54 | viewModelScope.launch { 55 | uiState = uiState.copy( 56 | loading = false, 57 | logs = db.logDao().getAllWithFiles(photoSaver.photoFolder) 58 | ) 59 | } 60 | } 61 | 62 | fun delete(log: Log) { 63 | viewModelScope.launch { 64 | db.logDao().delete(log.toLogEntry()) 65 | loadLogs() 66 | } 67 | } 68 | } 69 | 70 | class HomeViewModelFactory : ViewModelProvider.Factory { 71 | @Suppress("UNCHECKED_CAST") 72 | override fun create(modelClass: Class, extras: CreationExtras): T { 73 | val app = 74 | extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as PhotoGoodApplication 75 | return HomeViewModel(app, app.photoSaver) as T 76 | } 77 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/Log.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import java.io.File 20 | import java.text.SimpleDateFormat 21 | import java.util.Locale 22 | 23 | data class Log( 24 | val date: String, 25 | val place: String, 26 | val photos: List 27 | ) { 28 | val timeInMillis = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(date)!!.time 29 | 30 | fun toLogEntry(): LogEntry { 31 | return LogEntry( 32 | date = date, 33 | place = place, 34 | photo1 = photos[0].name, 35 | photo2 = photos.getOrNull(1)?.name, 36 | photo3 = photos.getOrNull(2)?.name 37 | ) 38 | } 39 | 40 | companion object { 41 | fun fromLogEntry(logEntry: LogEntry, photoFolder: File): Log { 42 | return Log( 43 | date = logEntry.date, 44 | place = logEntry.place, 45 | photos = listOfNotNull(logEntry.photo1, logEntry.photo2, logEntry.photo3).map { 46 | File(photoFolder, it) 47 | } 48 | ) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/LogDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import androidx.room.Dao 20 | import androidx.room.Delete 21 | import androidx.room.Insert 22 | import androidx.room.OnConflictStrategy 23 | import androidx.room.Query 24 | import java.io.File 25 | 26 | @Dao 27 | interface LogDao { 28 | @Query("SELECT * FROM logs ORDER BY date DESC") 29 | suspend fun getAll(): List 30 | 31 | suspend fun getAllWithFiles(photoFolder: File): List { 32 | return getAll().map { Log.fromLogEntry(it, photoFolder) } 33 | } 34 | 35 | @Insert(onConflict = OnConflictStrategy.REPLACE) 36 | suspend fun insert(log: LogEntry) 37 | 38 | @Delete 39 | suspend fun delete(log: LogEntry) 40 | } 41 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/LogEntry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import androidx.room.ColumnInfo 20 | import androidx.room.Entity 21 | import androidx.room.PrimaryKey 22 | 23 | @Entity(tableName = "logs") 24 | data class LogEntry( 25 | @PrimaryKey val date: String, // date format: yyyy-MM-dd 26 | @ColumnInfo(name = "place") val place: String, 27 | @ColumnInfo(name = "photo1_name") val photo1: String, 28 | @ColumnInfo(name = "photo2_name") val photo2: String? = null, 29 | @ColumnInfo(name = "photo3_name") val photo3: String? = null 30 | ) 31 | 32 | const val MAX_LOG_PHOTOS_LIMIT = 3 -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.app.AppOpsManager 20 | import android.app.AppOpsManager.OPSTR_CAMERA 21 | import android.app.AppOpsManager.OPSTR_COARSE_LOCATION 22 | import android.app.AppOpsManager.OPSTR_FINE_LOCATION 23 | import android.app.AsyncNotedAppOp 24 | import android.app.SyncNotedAppOp 25 | import android.os.Build 26 | import android.os.Bundle 27 | import androidx.activity.ComponentActivity 28 | import androidx.activity.compose.setContent 29 | import androidx.annotation.RequiresApi 30 | import androidx.compose.foundation.layout.fillMaxSize 31 | import androidx.compose.material3.MaterialTheme 32 | import androidx.compose.material3.Surface 33 | import androidx.compose.ui.Modifier 34 | import androidx.lifecycle.lifecycleScope 35 | import androidx.navigation.compose.NavHost 36 | import androidx.navigation.compose.composable 37 | import androidx.navigation.compose.rememberNavController 38 | import com.example.photolog_end.ui.theme.PhotoGoodTheme 39 | import kotlinx.coroutines.launch 40 | import android.util.Log 41 | 42 | class MainActivity : ComponentActivity() { 43 | lateinit var permissionManager: PermissionManager 44 | 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | permissionManager = (application as PhotoGoodApplication).permissions 48 | 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 50 | val appOpsManager = getSystemService(AppOpsManager::class.java) as AppOpsManager 51 | appOpsManager.setOnOpNotedCallback(mainExecutor, DataAccessAuditListener) 52 | } 53 | 54 | setContent { 55 | PhotoGoodTheme { 56 | // A surface container using the 'background' color from the theme 57 | Surface( 58 | modifier = Modifier.fillMaxSize(), 59 | color = MaterialTheme.colorScheme.background 60 | ) { 61 | val navController = rememberNavController() 62 | 63 | val startNavigation = Screens.Home.route 64 | 65 | NavHost(navController = navController, startDestination = startNavigation) { 66 | composable(Screens.Home.route) { HomeScreen(navController) } 67 | composable(Screens.AddLog.route) { AddLogScreen(navController) } 68 | composable(Screens.Camera.route) { CameraScreen(navController) } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | override fun onResume() { 76 | super.onResume() 77 | lifecycleScope.launch { 78 | permissionManager.checkPermissions() 79 | } 80 | } 81 | 82 | @RequiresApi(Build.VERSION_CODES.R) 83 | object DataAccessAuditListener : AppOpsManager.OnOpNotedCallback() { 84 | // note: we are just logging to console for this codelab but you can also integrate 85 | // other logging and reporting systems here to track your app's private data access. 86 | override fun onNoted(op: SyncNotedAppOp) { 87 | Log.d("DataAccessAuditListener","Sync Private Data Accessed: ${op.op}") 88 | } 89 | 90 | override fun onSelfNoted(op: SyncNotedAppOp) { 91 | Log.d("DataAccessAuditListener","Self Private Data accessed: ${op.op}") 92 | } 93 | 94 | override fun onAsyncNoted(asyncNotedAppOp: AsyncNotedAppOp) { 95 | var emoji = when (asyncNotedAppOp.op) { 96 | OPSTR_COARSE_LOCATION -> "\uD83D\uDDFA" 97 | OPSTR_CAMERA -> "\uD83D\uDCF8" 98 | else -> "?" 99 | } 100 | 101 | Log.d("DataAccessAuditListener", "Async Private Data ($emoji) Accessed: ${asyncNotedAppOp.op}") 102 | } 103 | } 104 | } 105 | 106 | sealed class Screens(val route: String) { 107 | object Permissions : Screens("permissions") 108 | object Home : Screens("home") 109 | object AddLog : Screens("add_log") 110 | object Camera : Screens("camera") 111 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/MediaRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.content.ContentUris 20 | import android.content.Context 21 | import android.net.Uri 22 | import android.provider.MediaStore 23 | import android.provider.MediaStore.Files.FileColumns.DATA 24 | import android.provider.MediaStore.Files.FileColumns.DATE_ADDED 25 | import android.provider.MediaStore.Files.FileColumns.DISPLAY_NAME 26 | import android.provider.MediaStore.Files.FileColumns.MIME_TYPE 27 | import android.provider.MediaStore.Files.FileColumns.SIZE 28 | import android.provider.MediaStore.Files.FileColumns._ID 29 | import kotlinx.coroutines.flow.flow 30 | import java.io.File 31 | 32 | class MediaRepository(private val context: Context) { 33 | data class MediaEntry( 34 | val uri: Uri, 35 | val filename: String, 36 | val mimeType: String, 37 | val size: Long, 38 | val path: String 39 | ) { 40 | val file: File 41 | get() = File(path) 42 | } 43 | 44 | fun fetchImages() = flow { 45 | val externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI 46 | 47 | val projection = arrayOf( 48 | _ID, 49 | DISPLAY_NAME, 50 | SIZE, 51 | MIME_TYPE, 52 | DATA, 53 | ) 54 | 55 | val cursor = context.contentResolver.query( 56 | externalContentUri, 57 | projection, 58 | null, 59 | null, 60 | "$DATE_ADDED DESC" 61 | ) ?: throw Exception("Query could not be executed") 62 | 63 | cursor.use { 64 | while (cursor.moveToNext()) { 65 | val idColumn = cursor.getColumnIndexOrThrow(_ID) 66 | val displayNameColumn = cursor.getColumnIndexOrThrow(DISPLAY_NAME) 67 | val sizeColumn = cursor.getColumnIndexOrThrow(SIZE) 68 | val mimeTypeColumn = cursor.getColumnIndexOrThrow(MIME_TYPE) 69 | val dataColumn = cursor.getColumnIndexOrThrow(DATA) 70 | 71 | val contentUri = ContentUris.withAppendedId( 72 | externalContentUri, 73 | cursor.getLong(idColumn) 74 | ) 75 | 76 | emit( 77 | MediaEntry( 78 | uri = contentUri, 79 | filename = cursor.getString(displayNameColumn), 80 | size = cursor.getLong(sizeColumn), 81 | mimeType = cursor.getString(mimeTypeColumn), 82 | path = cursor.getString(dataColumn), 83 | ) 84 | ) 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/PermissionManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.Manifest 20 | import android.Manifest.permission.ACCESS_COARSE_LOCATION 21 | import android.Manifest.permission.ACCESS_FINE_LOCATION 22 | import android.Manifest.permission.CAMERA 23 | import android.Manifest.permission.READ_EXTERNAL_STORAGE 24 | import android.Manifest.permission.READ_MEDIA_IMAGES 25 | import android.content.Context 26 | import android.content.Intent 27 | import android.content.pm.PackageManager 28 | import android.net.Uri 29 | import android.provider.Settings 30 | import androidx.core.content.ContextCompat 31 | import kotlinx.coroutines.flow.MutableStateFlow 32 | import kotlinx.coroutines.flow.asStateFlow 33 | 34 | class PermissionManager(private val context: Context) { 35 | companion object { 36 | val REQUIRED_PERMISSIONS_PRE_T = arrayOf( 37 | READ_EXTERNAL_STORAGE, 38 | CAMERA, 39 | ACCESS_FINE_LOCATION, 40 | ACCESS_COARSE_LOCATION 41 | ) 42 | val REQUIRED_PERMISSIONS_POST_T = arrayOf( 43 | READ_MEDIA_IMAGES, 44 | CAMERA, 45 | ACCESS_FINE_LOCATION, 46 | ACCESS_COARSE_LOCATION 47 | ) 48 | } 49 | 50 | data class State( 51 | val hasStorageAccess: Boolean, 52 | val hasCameraAccess: Boolean, 53 | val hasLocationAccess: Boolean 54 | ) { 55 | val hasAllAccess: Boolean 56 | get() = hasStorageAccess && hasCameraAccess && hasLocationAccess 57 | } 58 | 59 | private val _state = MutableStateFlow( 60 | State( 61 | hasStorageAccess = hasAccess(READ_EXTERNAL_STORAGE) || hasAccess(Manifest.permission.READ_MEDIA_IMAGES), 62 | hasCameraAccess = hasAccess(CAMERA), 63 | hasLocationAccess = hasAccess(listOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)), 64 | ) 65 | ) 66 | val state = _state.asStateFlow() 67 | val hasAllPermissions: Boolean 68 | get() = _state.value.hasAllAccess 69 | 70 | private fun hasAccess(permission: String): Boolean { 71 | return ContextCompat.checkSelfPermission( 72 | context, 73 | permission 74 | ) == PackageManager.PERMISSION_GRANTED 75 | } 76 | 77 | private fun hasAccess(permissions: List): Boolean { 78 | return permissions.all(::hasAccess) 79 | } 80 | 81 | fun onPermissionChange(permissions: Map) { 82 | val hasLocationAccess = hasAccess(ACCESS_FINE_LOCATION) && hasAccess(ACCESS_COARSE_LOCATION) 83 | val hasStorageAccess = hasAccess(Manifest.permission.READ_MEDIA_IMAGES) || hasAccess(READ_EXTERNAL_STORAGE) 84 | 85 | _state.value = State( 86 | hasStorageAccess = hasStorageAccess, 87 | hasCameraAccess = permissions[CAMERA] ?: _state.value.hasCameraAccess, 88 | hasLocationAccess = hasLocationAccess 89 | ) 90 | } 91 | 92 | suspend fun checkPermissions() { 93 | val newState = State( 94 | hasStorageAccess = hasAccess(READ_EXTERNAL_STORAGE) || hasAccess(Manifest.permission.READ_MEDIA_IMAGES), 95 | hasCameraAccess = hasAccess(CAMERA), 96 | hasLocationAccess = hasAccess(ACCESS_FINE_LOCATION) && hasAccess(ACCESS_COARSE_LOCATION) 97 | ) 98 | 99 | _state.emit(newState) 100 | } 101 | 102 | fun createSettingsIntent(): Intent { 103 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 104 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 105 | data = Uri.fromParts("package", context.packageName, null) 106 | } 107 | 108 | return intent 109 | } 110 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/PermissionScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.os.Build 20 | import androidx.activity.compose.rememberLauncherForActivityResult 21 | import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions 22 | import androidx.compose.foundation.layout.Arrangement 23 | import androidx.compose.foundation.layout.Column 24 | import androidx.compose.foundation.layout.Spacer 25 | import androidx.compose.foundation.layout.fillMaxSize 26 | import androidx.compose.foundation.layout.height 27 | import androidx.compose.foundation.layout.padding 28 | import androidx.compose.material.icons.Icons 29 | import androidx.compose.material.icons.filled.Camera 30 | import androidx.compose.material.icons.filled.Check 31 | import androidx.compose.material.icons.filled.Close 32 | import androidx.compose.material.icons.filled.Explore 33 | import androidx.compose.material.icons.filled.PhotoLibrary 34 | import androidx.compose.material3.Divider 35 | import androidx.compose.material3.ExperimentalMaterial3Api 36 | import androidx.compose.material3.FilledTonalButton 37 | import androidx.compose.material3.Icon 38 | import androidx.compose.material3.ListItem 39 | import androidx.compose.material3.MaterialTheme 40 | import androidx.compose.material3.Scaffold 41 | import androidx.compose.material3.SmallTopAppBar 42 | import androidx.compose.material3.Text 43 | import androidx.compose.runtime.Composable 44 | import androidx.compose.runtime.collectAsState 45 | import androidx.compose.runtime.getValue 46 | import androidx.compose.runtime.mutableStateOf 47 | import androidx.compose.runtime.remember 48 | import androidx.compose.runtime.setValue 49 | import androidx.compose.ui.Alignment 50 | import androidx.compose.ui.Modifier 51 | import androidx.compose.ui.platform.LocalContext 52 | import androidx.compose.ui.text.font.FontFamily 53 | import androidx.compose.ui.unit.dp 54 | import androidx.core.content.ContextCompat 55 | import androidx.lifecycle.viewmodel.compose.viewModel 56 | import androidx.navigation.NavHostController 57 | import com.example.photolog_end.PermissionManager.Companion.REQUIRED_PERMISSIONS_PRE_T 58 | import com.example.photolog_end.PermissionManager.Companion.REQUIRED_PERMISSIONS_POST_T 59 | 60 | @OptIn(ExperimentalMaterial3Api::class) 61 | @Composable 62 | fun PermissionScreen( 63 | navController: NavHostController, 64 | viewModel: PermissionViewModel = viewModel(factory = PermissionViewModelFactory()) 65 | ) { 66 | 67 | val state = viewModel.uiState.collectAsState() 68 | val context = LocalContext.current 69 | var hasRequestedPermissions by remember { mutableStateOf(false) } 70 | 71 | val requestPermissions = 72 | rememberLauncherForActivityResult(RequestMultiplePermissions()) { permissions -> 73 | hasRequestedPermissions = true 74 | viewModel.onPermissionChange(permissions) 75 | } 76 | 77 | fun openSettings() { 78 | ContextCompat.startActivity(context, viewModel.createSettingsIntent(), null) 79 | } 80 | 81 | Scaffold( 82 | topBar = { 83 | SmallTopAppBar( 84 | title = { Text("Permissions needed", fontFamily = FontFamily.Serif) } 85 | ) 86 | } 87 | ) { innerPadding -> 88 | Column( 89 | modifier = Modifier 90 | .padding(innerPadding) 91 | .fillMaxSize(), 92 | verticalArrangement = Arrangement.Center, 93 | horizontalAlignment = Alignment.CenterHorizontally 94 | ) { 95 | Text( 96 | modifier = Modifier.padding(16.dp), 97 | text = "You have to grant access to these permissions in order to use the app" 98 | ) 99 | ListItem( 100 | headlineText = { Text("Storage access") }, 101 | supportingText = { Text("Add photos from library when creating a log") }, 102 | trailingContent = { PermissionAccessIcon(state.value.hasStorageAccess) }, 103 | leadingContent = { 104 | Icon( 105 | Icons.Filled.PhotoLibrary, 106 | contentDescription = null, 107 | tint = MaterialTheme.colorScheme.surfaceTint 108 | ) 109 | } 110 | ) 111 | Divider() 112 | ListItem( 113 | headlineText = { Text("Camera access") }, 114 | supportingText = { Text("Take picture when creating a log") }, 115 | trailingContent = { PermissionAccessIcon(state.value.hasCameraAccess) }, 116 | leadingContent = { 117 | Icon( 118 | Icons.Filled.Camera, 119 | contentDescription = null, 120 | tint = MaterialTheme.colorScheme.surfaceTint 121 | ) 122 | } 123 | ) 124 | Divider() 125 | ListItem( 126 | headlineText = { Text("Precise location access") }, 127 | supportingText = { Text("Keep track of the location of a log") }, 128 | trailingContent = { PermissionAccessIcon(state.value.hasLocationAccess) }, 129 | leadingContent = { 130 | Icon( 131 | Icons.Filled.Explore, 132 | contentDescription = null, 133 | tint = MaterialTheme.colorScheme.surfaceTint 134 | ) 135 | } 136 | ) 137 | Spacer(Modifier.height(32.dp)) 138 | if (state.value.hasAllAccess) { 139 | FilledTonalButton(onClick = { navController.navigate(Screens.Home.route) }) { 140 | Text("Get started") 141 | } 142 | } else { 143 | if (hasRequestedPermissions) { 144 | FilledTonalButton(onClick = { openSettings() }) { 145 | Text("Go to settings") 146 | } 147 | } else { 148 | FilledTonalButton(onClick = { 149 | if (Build.VERSION.SDK_INT >= 33) { 150 | requestPermissions.launch(REQUIRED_PERMISSIONS_POST_T) 151 | } 152 | else { 153 | requestPermissions.launch(REQUIRED_PERMISSIONS_PRE_T) 154 | } 155 | }) { 156 | Text("Request permissions") 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | @Composable 165 | fun PermissionAccessIcon(hasAccess: Boolean) { 166 | if (hasAccess) { 167 | Icon( 168 | Icons.Filled.Check, 169 | contentDescription = "Permission accepted" 170 | ) 171 | } else { 172 | Icon( 173 | Icons.Filled.Close, 174 | contentDescription = "Permission not granted" 175 | ) 176 | } 177 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/PermissionViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.content.Intent 20 | import androidx.lifecycle.ViewModel 21 | import androidx.lifecycle.ViewModelProvider 22 | import androidx.lifecycle.viewmodel.CreationExtras 23 | 24 | class PermissionViewModel(val permissions: PermissionManager) : ViewModel() { 25 | 26 | val uiState = permissions.state 27 | 28 | fun onPermissionChange(requestedPermissions: Map) { 29 | permissions.onPermissionChange(requestedPermissions) 30 | } 31 | 32 | fun createSettingsIntent(): Intent { 33 | return permissions.createSettingsIntent() 34 | } 35 | } 36 | 37 | class PermissionViewModelFactory : ViewModelProvider.Factory { 38 | @Suppress("UNCHECKED_CAST") 39 | override fun create(modelClass: Class, extras: CreationExtras): T { 40 | val app = 41 | extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as PhotoGoodApplication 42 | return PermissionViewModel(app.permissions) as T 43 | } 44 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/PhotoGoodApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.app.Application 20 | 21 | class PhotoGoodApplication : Application() { 22 | lateinit var photoSaver: PhotoSaverRepository 23 | lateinit var permissions: PermissionManager 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | 28 | photoSaver = PhotoSaverRepository(this, this.contentResolver) 29 | permissions = PermissionManager(this) 30 | } 31 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/PhotoGrid.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.Row 21 | import androidx.compose.foundation.layout.Spacer 22 | import androidx.compose.foundation.layout.aspectRatio 23 | import androidx.compose.foundation.layout.width 24 | import androidx.compose.foundation.shape.RoundedCornerShape 25 | import androidx.compose.material.icons.Icons 26 | import androidx.compose.material.icons.filled.Close 27 | import androidx.compose.material3.ExperimentalMaterial3Api 28 | import androidx.compose.material3.FilledTonalIconButton 29 | import androidx.compose.material3.Icon 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.draw.clip 34 | import androidx.compose.ui.layout.ContentScale 35 | import androidx.compose.ui.unit.dp 36 | import coil.compose.AsyncImage 37 | import java.io.File 38 | 39 | @OptIn(ExperimentalMaterial3Api::class) 40 | @Composable 41 | fun PhotoGrid( 42 | modifier: Modifier, 43 | photos: List, 44 | onRemove: ((photo: File) -> Unit)? = null 45 | ) { 46 | Row(modifier) { 47 | repeat(MAX_LOG_PHOTOS_LIMIT) { index -> 48 | val file = photos.getOrNull(index) 49 | 50 | if (file == null) { 51 | Box(Modifier.weight(1f)) 52 | } else { 53 | Box( 54 | contentAlignment = Alignment.TopEnd, 55 | modifier = Modifier 56 | .weight(1f) 57 | .clip(RoundedCornerShape(10.dp)) 58 | .aspectRatio(1f) 59 | ) { 60 | AsyncImage( 61 | model = file, 62 | contentDescription = null, 63 | contentScale = ContentScale.Crop 64 | ) 65 | 66 | if (onRemove != null) { 67 | FilledTonalIconButton(onClick = { onRemove(file) }) { 68 | Icon(Icons.Filled.Close, null) 69 | } 70 | } 71 | } 72 | } 73 | Spacer(Modifier.width(8.dp)) 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/PhotoSaverRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end 18 | 19 | import android.content.ContentResolver 20 | import android.content.Context 21 | import android.net.Uri 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.withContext 24 | import java.io.File 25 | 26 | class PhotoSaverRepository(context: Context, private val contentResolver: ContentResolver) { 27 | 28 | private val _photos = mutableListOf() 29 | 30 | fun getPhotos() = _photos.toList() 31 | fun isEmpty() = _photos.isEmpty() 32 | fun canAddPhoto() = _photos.size < MAX_LOG_PHOTOS_LIMIT 33 | 34 | private val cacheFolder = File(context.cacheDir, "photos").also { it.mkdir() } 35 | val photoFolder = File(context.filesDir, "photos").also { it.mkdir() } 36 | 37 | private fun generateFileName() = "${System.currentTimeMillis()}.jpg" 38 | private fun generatePhotoLogFile() = File(photoFolder, generateFileName()) 39 | fun generatePhotoCacheFile() = File(cacheFolder, generateFileName()) 40 | 41 | fun cacheCapturedPhoto(photo: File) { 42 | if (_photos.size + 1 > MAX_LOG_PHOTOS_LIMIT) { 43 | return 44 | } 45 | 46 | _photos += photo 47 | } 48 | 49 | @Suppress("BlockingMethodInNonBlockingContext") 50 | suspend fun cacheFromUri(uri: Uri) { 51 | withContext(Dispatchers.IO) { 52 | if (_photos.size + 1 > MAX_LOG_PHOTOS_LIMIT) { 53 | return@withContext 54 | } 55 | 56 | contentResolver.openInputStream(uri)?.use { input -> 57 | val cachedPhoto = generatePhotoCacheFile() 58 | 59 | cachedPhoto.outputStream().use { output -> 60 | input.copyTo(output) 61 | _photos += cachedPhoto 62 | } 63 | } 64 | } 65 | } 66 | 67 | suspend fun cacheFromUris(uris: List) { 68 | uris.forEach { 69 | cacheFromUri(it) 70 | } 71 | } 72 | 73 | suspend fun removeFile(photo: File) { 74 | withContext(Dispatchers.IO) { 75 | photo.delete() 76 | _photos -= photo 77 | } 78 | } 79 | 80 | suspend fun savePhotos(): List { 81 | return withContext(Dispatchers.IO) { 82 | val savedPhotos = _photos.map { it.copyTo(generatePhotoLogFile()) } 83 | 84 | _photos.forEach { it.delete() } 85 | _photos.clear() 86 | 87 | savedPhotos 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end.ui.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | val Purple80 = Color(0xFFD0BCFF) 22 | val PurpleGrey80 = Color(0xFFCCC2DC) 23 | val Pink80 = Color(0xFFEFB8C8) 24 | 25 | val Purple40 = Color(0xFF6650a4) 26 | val PurpleGrey40 = Color(0xFF625b71) 27 | val Pink40 = Color(0xFF7D5260) 28 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end.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.ViewCompat 33 | 34 | private val DarkColorScheme = darkColorScheme( 35 | primary = Purple80, 36 | secondary = PurpleGrey80, 37 | tertiary = Pink80 38 | ) 39 | 40 | private val LightColorScheme = lightColorScheme( 41 | primary = Purple40, 42 | secondary = PurpleGrey40, 43 | tertiary = Pink40 44 | 45 | /* Other default colors to override 46 | background = Color(0xFFFFFBFE), 47 | surface = Color(0xFFFFFBFE), 48 | onPrimary = Color.White, 49 | onSecondary = Color.White, 50 | onTertiary = Color.White, 51 | onBackground = Color(0xFF1C1B1F), 52 | onSurface = Color(0xFF1C1B1F), 53 | */ 54 | ) 55 | 56 | @Composable 57 | fun PhotoGoodTheme( 58 | darkTheme: Boolean = isSystemInDarkTheme(), 59 | // Dynamic color is available on Android 12+ 60 | dynamicColor: Boolean = true, 61 | content: @Composable () -> Unit 62 | ) { 63 | val colorScheme = when { 64 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 65 | val context = LocalContext.current 66 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 67 | } 68 | darkTheme -> DarkColorScheme 69 | else -> LightColorScheme 70 | } 71 | val view = LocalView.current 72 | if (!view.isInEditMode) { 73 | SideEffect { 74 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 75 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 76 | } 77 | } 78 | 79 | MaterialTheme( 80 | colorScheme = colorScheme, 81 | typography = Typography, 82 | content = content 83 | ) 84 | } -------------------------------------------------------------------------------- /PhotoLog_End/src/main/java/com/example/photolog_end/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 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.photolog_end.ui.theme 18 | 19 | import androidx.compose.material3.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.sp 24 | 25 | // Set of Material typography styles to start with 26 | val Typography = Typography( 27 | bodyLarge = TextStyle( 28 | fontFamily = FontFamily.Default, 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 16.sp, 31 | lineHeight = 24.sp, 32 | letterSpacing = 0.5.sp 33 | ) 34 | /* Other default text styles to override 35 | titleLarge = TextStyle( 36 | fontFamily = FontFamily.Default, 37 | fontWeight = FontWeight.Normal, 38 | fontSize = 22.sp, 39 | lineHeight = 28.sp, 40 | letterSpacing = 0.sp 41 | ), 42 | labelSmall = TextStyle( 43 | fontFamily = FontFamily.Default, 44 | fontWeight = FontWeight.Medium, 45 | fontSize = 11.sp, 46 | lineHeight = 16.sp, 47 | letterSpacing = 0.5.sp 48 | ) 49 | */ 50 | ) 51 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 25 | 31 | 34 | 37 | 38 | 39 | 40 | 46 | -------------------------------------------------------------------------------- /PhotoLog_End/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 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/privacy-codelab/628e1192036c5bafb4f279fc4022dc5da3dca9ca/PhotoLog_End/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | #FFBB86FC 20 | #FF6200EE 21 | #FF3700B3 22 | #FF03DAC5 23 | #FF018786 24 | #FF000000 25 | #FFFFFFFF 26 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | PhotoGood 19 | -------------------------------------------------------------------------------- /PhotoLog_End/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 |