├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kiwi │ │ └── kiwitalk │ │ └── ExampleInstrumentedTest.kt │ ├── debug │ ├── ic_launcher-playstore.png │ └── res │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── layout_map_fragment_dummy.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── map_style.json │ ├── java │ │ └── com │ │ │ └── kiwi │ │ │ └── kiwitalk │ │ │ ├── KiwiApplication.kt │ │ │ ├── NetworkStateManager.kt │ │ │ ├── di │ │ │ ├── ApplicationModule.kt │ │ │ ├── ChatModule.kt │ │ │ ├── FirebaseModule.kt │ │ │ ├── KakaoApiModule.kt │ │ │ └── RepositoryModule.kt │ │ │ ├── model │ │ │ └── ClusterMarker.kt │ │ │ ├── ui │ │ │ ├── BindingAdapter.kt │ │ │ ├── chatting │ │ │ │ └── ChattingActivity.kt │ │ │ ├── home │ │ │ │ ├── ChatListFragment.kt │ │ │ │ ├── ChatListViewAdapter.kt │ │ │ │ ├── ChatListViewModel.kt │ │ │ │ ├── HomeActivity.kt │ │ │ │ ├── ProfileSettingFragment.kt │ │ │ │ └── ProfileViewModel.kt │ │ │ ├── keyword │ │ │ │ ├── SearchKeywordFragment.kt │ │ │ │ ├── SearchKeywordViewModel.kt │ │ │ │ └── recyclerview │ │ │ │ │ ├── KeywordAdapter.kt │ │ │ │ │ ├── KeywordCategoryAdapter.kt │ │ │ │ │ └── SelectedKeywordAdapter.kt │ │ │ ├── login │ │ │ │ ├── LoginActivity.kt │ │ │ │ ├── LoginViewModel.kt │ │ │ │ ├── ProgressDialog.kt │ │ │ │ ├── SplashActivity.kt │ │ │ │ └── SplashViewModel.kt │ │ │ ├── newchat │ │ │ │ ├── NewChatActivity.kt │ │ │ │ ├── NewChatFragment.kt │ │ │ │ ├── NewChatViewModel.kt │ │ │ │ ├── SearchPlaceFragment.kt │ │ │ │ └── SearchPlaceViewModel.kt │ │ │ └── search │ │ │ │ ├── ChatAdapter.kt │ │ │ │ ├── ChatInfoViewHolder.kt │ │ │ │ ├── ChatJoinDialog.kt │ │ │ │ ├── ClusterMarkerRenderer.kt │ │ │ │ ├── SearchChatActivity.kt │ │ │ │ ├── SearchChatMapFragment.kt │ │ │ │ └── SearchChatMapViewModel.kt │ │ │ └── util │ │ │ ├── ChangeExpansion.kt │ │ │ ├── Const.kt │ │ │ ├── Event.kt │ │ │ └── Util.kt │ └── res │ │ ├── color │ │ ├── color_chip_background_keyword.xml │ │ └── color_chip_text_keyword.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── background_marker_text.xml │ │ ├── bg_chat_list_unread_count.xml │ │ ├── ic_add_location.xml │ │ ├── ic_add_photo.xml │ │ ├── ic_backarrow_48.xml │ │ ├── ic_baseline_cloud_sync_24.xml │ │ ├── ic_chatlist_searchchat.png │ │ ├── ic_launcher_background.xml │ │ ├── ic_location_cluster.xml │ │ ├── ic_location_on.xml │ │ ├── ic_location_on_click.xml │ │ ├── ic_profile_base_image.xml │ │ ├── ic_search.xml │ │ ├── ic_search_chat_new_chat.png │ │ ├── logo_splash_dark.png │ │ ├── logo_splash_light.png │ │ ├── logo_splash_transparent.png │ │ └── logo_splash_withtext.png │ │ ├── layout │ │ ├── activity_chatting.xml │ │ ├── activity_home.xml │ │ ├── activity_login.xml │ │ ├── activity_new_chat.xml │ │ ├── activity_search_chat.xml │ │ ├── activity_splash.xml │ │ ├── dialog_join_chat.xml │ │ ├── dialog_login_progress.xml │ │ ├── dialog_new_chat.xml │ │ ├── fragment_chat_list.xml │ │ ├── fragment_new_chat.xml │ │ ├── fragment_profile_setting.xml │ │ ├── fragment_search_chat_map.xml │ │ ├── fragment_search_keyword.xml │ │ ├── fragment_search_place.xml │ │ ├── item_chat_list.xml │ │ ├── item_chat_list_no_image.xml │ │ ├── item_keyword.xml │ │ ├── item_keywordcategory.xml │ │ ├── item_marker.xml │ │ ├── view_marker_detail.xml │ │ └── view_marker_preview.xml │ │ ├── menu │ │ ├── menu_chat_list.xml │ │ ├── menu_new_chat.xml │ │ ├── menu_search_chat_map.xml │ │ └── menu_search_keyword_toolbar.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 │ │ ├── navigation │ │ ├── home_nav_graph.xml │ │ ├── new_chat_nav_graph.xml │ │ └── search_chat_nav_graph.xml │ │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── style.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── kiwi │ └── kiwitalk │ └── ExampleUnitTest.kt ├── build.gradle ├── chatmapper ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── kiwi │ └── chatmapper │ ├── ChatConst.kt │ ├── ChatKey.kt │ └── ChatMapper.kt ├── data ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── google-services.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kiwi │ │ └── data │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── kiwi │ │ └── data │ │ ├── AppPreference.kt │ │ ├── Const.kt │ │ ├── UserDataCallback.kt │ │ ├── datasource │ │ ├── local │ │ │ ├── UserLocalDataSource.kt │ │ │ └── UserLocalDataSourceImpl.kt │ │ └── remote │ │ │ ├── ExitChatRemoteDataSource.kt │ │ │ ├── ExitChatRemoteDataSourceImpl.kt │ │ │ ├── NewChatRemoteDataSource.kt │ │ │ ├── NewChatRemoteDataSourceImpl.kt │ │ │ ├── SearchChatRemoteDataSource.kt │ │ │ ├── SearchChatRemoteDataSourceImpl.kt │ │ │ ├── SearchKeywordRemoteDataSource.kt │ │ │ ├── SearchPlaceRemoteDataSource.kt │ │ │ ├── SearchPlaceRemoteDataSourceImpl.kt │ │ │ ├── SearchService.kt │ │ │ ├── UserRemoteDataSource.kt │ │ │ └── UserRemoteDataSourceImpl.kt │ │ ├── mapper │ │ └── Mapper.kt │ │ ├── model │ │ └── remote │ │ │ ├── MarkerRemote.kt │ │ │ ├── NewChatRemote.kt │ │ │ ├── PlaceListRemote.kt │ │ │ └── PlaceRemote.kt │ │ └── repository │ │ ├── ExitChatRepositoryImpl.kt │ │ ├── NewChatRepositoryImpl.kt │ │ ├── ProfileSettingRepositoryImpl.kt │ │ ├── SearchChatRepositoryImpl.kt │ │ ├── SearchKeywordRepositoryImpl.kt │ │ ├── SearchPlaceRepositoryImpl.kt │ │ └── UserRepositoryImpl.kt │ └── test │ └── java │ └── com │ └── kiwi │ └── data │ └── ExampleUnitTest.kt ├── domain ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── kiwi │ └── domain │ ├── UserUiCallback.kt │ ├── model │ ├── ChatInfo.kt │ ├── Keyword.kt │ ├── KeywordCategory.kt │ ├── Marker.kt │ ├── NewChatInfo.kt │ ├── PlaceChatInfo.kt │ ├── PlaceInfo.kt │ ├── PlaceInfoList.kt │ └── UserInfo.kt │ └── repository │ ├── ExitChatRepository.kt │ ├── NewChatRepository.kt │ ├── ProfileSettingRepository.kt │ ├── SearchChatRepository.kt │ ├── SearchKeywordRepository.kt │ ├── SearchPlaceRepository.kt │ └── UserRepository.kt ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.jar ├── gradlew ├── gradlew.bat └── settings.gradle /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | misc.xml 22 | deploymentTargetDropDown.xml 23 | render.experimental.xml 24 | 25 | # Keystore files 26 | *.jks 27 | *.keystore 28 | 29 | # Android Profiling 30 | *.hprof 31 | 32 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio,kotlin 33 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,kotlin 34 | ### Kotlin ### 35 | # Compiled class file 36 | *.class 37 | # Log file 38 | # BlueJ files 39 | *.ctxt 40 | # Mobile Tools for Java (J2ME) 41 | .mtj.tmp/ 42 | # Package Files # 43 | *.jar 44 | *.war 45 | *.nar 46 | *.ear 47 | *.zip 48 | *.tar.gz 49 | *.rar 50 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 51 | hs_err_pid* 52 | replay_pid* 53 | ### AndroidStudio ### 54 | # Covers files to be ignored for android development using Android Studio. 55 | # Built application files 56 | *.ap_ 57 | *.aab 58 | # Files for the ART/Dalvik VM 59 | *.dex 60 | # Java class files 61 | # Generated files 62 | bin/ 63 | gen/ 64 | out/ 65 | # Gradle files 66 | .gradle 67 | # Signing files 68 | .signing/ 69 | # Local configuration file (sdk path, etc) 70 | # Proguard folder generated by Eclipse 71 | proguard/ 72 | # Log Files 73 | # Android Studio 74 | /*/build/ 75 | /*/local.properties 76 | /*/out 77 | /*/*/build 78 | /*/*/production 79 | 80 | .navigation/ 81 | *.ipr 82 | *~ 83 | *.swp 84 | # Android Patch 85 | gen-external-apklibs 86 | # External native build folder generated in Android Studio 2.2 and later 87 | .externalNativeBuild 88 | # NDK 89 | obj/ 90 | # IntelliJ IDEA 91 | 92 | *.iws 93 | /out/ 94 | # Legacy Eclipse project files 95 | .classpath 96 | .project 97 | .cproject 98 | .settings/ 99 | # Mobile Tools for Java (J2ME) 100 | # Package Files # 101 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 102 | ## Plugin-specific files: 103 | # mpeltonen/sbt-idea plugin 104 | .idea_modules/ 105 | # JIRA plugin 106 | atlassian-ide-plugin.xml 107 | # Mongo Explorer plugin 108 | .idea/mongoSettings.xml 109 | # Crashlytics plugin (for Android Studio and IntelliJ) 110 | com_crashlytics_export_strings.xml 111 | crashlytics.properties 112 | crashlytics-build.properties 113 | fabric.properties 114 | ### AndroidStudio Patch ### 115 | !/gradle/wrapper/gradle-wrapper.jar 116 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio,kotlin 117 | 118 | 119 | # Mac .DS_Store 120 | .DS_Store 121 | 122 | # 기타 발견된 항목 123 | gradle/wrapper/gradle-wrapper.properties 124 | 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 부스트캠프 웹·모바일 7기 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'com.google.dagger.hilt.android' 6 | id 'com.google.gms.google-services' 7 | id 'androidx.navigation.safeargs.kotlin' 8 | id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' 9 | } 10 | 11 | android { 12 | compileSdk 33 13 | 14 | defaultConfig { 15 | applicationId "com.kiwi.kiwitalk" 16 | minSdk 21 17 | targetSdk 33 18 | versionCode 1 19 | versionName "1.0.0" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled false 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | buildFeatures { 38 | dataBinding true 39 | viewBinding true 40 | } 41 | } 42 | 43 | dependencies { 44 | api project(":data") 45 | implementation(project(":domain")) 46 | implementation(project(":chatmapper")) 47 | 48 | implementation 'androidx.core:core-ktx:1.9.0' 49 | implementation 'androidx.appcompat:appcompat:1.5.1' 50 | implementation 'com.google.android.material:material:1.7.0' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 52 | testImplementation 'junit:junit:4.13.2' 53 | androidTestImplementation 'androidx.test.ext:junit:1.1.4' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' 55 | 56 | implementation "androidx.activity:activity-ktx:1.6.1" 57 | implementation 'androidx.fragment:fragment-ktx:1.5.4' 58 | 59 | // stream chat 60 | def stream_version = "5.11.6" 61 | implementation "io.getstream:stream-chat-android-ui-components:$stream_version" 62 | 63 | // Hilt 64 | implementation "com.google.dagger:hilt-android:2.44" 65 | kapt "com.google.dagger:hilt-compiler:2.44" 66 | 67 | //FireBase 68 | implementation 'com.google.firebase:firebase-analytics:21.2.0'// 파이어베이스 앱 분석 69 | implementation 'com.google.firebase:firebase-core:21.1.1' //파이어베이스 코어 70 | implementation 'com.google.firebase:firebase-auth:21.1.0' //파이어베이스 인증 71 | implementation 'com.firebaseui:firebase-ui-auth:7.2.0' //파이어베이스 인증 2 72 | implementation 'com.google.firebase:firebase-firestore-ktx:24.4.1' //파이어스토어 73 | implementation 'com.google.firebase:firebase-storage-ktx:20.1.0' //파이어스토리지 74 | 75 | // navigation 76 | def nav_version = "2.5.3" 77 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" 78 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version" 79 | 80 | // 디바이스 위치 81 | implementation 'com.google.android.gms:play-services-maps:18.1.0' 82 | implementation 'com.google.android.gms:play-services-location:21.0.1' 83 | implementation 'com.google.maps.android:android-maps-utils:2.3.0' 84 | implementation 'com.google.maps.android:maps-ktx:3.4.0' 85 | implementation 'com.google.maps.android:maps-utils-ktx:3.4.0' 86 | 87 | // Glide 88 | implementation 'com.github.bumptech.glide:glide:4.14.2' 89 | annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2' 90 | 91 | // FlexBoxLayout 92 | implementation 'com.google.android.flexbox:flexbox:3.0.0' 93 | 94 | 95 | // retrofit 96 | def retrofit_version = "2.9.0" 97 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version" 98 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" 99 | implementation "com.squareup.okhttp3:logging-interceptor:4.10.0" 100 | } 101 | 102 | kapt { 103 | correctErrorTypes true 104 | } -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "611516619499", 4 | "project_id": "kiwi-talk-9249d", 5 | "storage_bucket": "kiwi-talk-9249d.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:611516619499:android:e87f8919813f13e8e27cae", 11 | "android_client_info": { 12 | "package_name": "com.kiwi.app" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "611516619499-26ld1il35ee8gil43it4e8q7nl8jbrf8.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyCAsKX4dhxkfR8c_ACsFGIMM8BzbjygxUg" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "611516619499-26ld1il35ee8gil43it4e8q7nl8jbrf8.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | }, 37 | { 38 | "client_info": { 39 | "mobilesdk_app_id": "1:611516619499:android:d74297d39b05e56de27cae", 40 | "android_client_info": { 41 | "package_name": "com.kiwi.kiwitalk" 42 | } 43 | }, 44 | "oauth_client": [ 45 | { 46 | "client_id": "611516619499-26ld1il35ee8gil43it4e8q7nl8jbrf8.apps.googleusercontent.com", 47 | "client_type": 3 48 | } 49 | ], 50 | "api_key": [ 51 | { 52 | "current_key": "AIzaSyCAsKX4dhxkfR8c_ACsFGIMM8BzbjygxUg" 53 | } 54 | ], 55 | "services": { 56 | "appinvite_service": { 57 | "other_platform_oauth_client": [ 58 | { 59 | "client_id": "611516619499-26ld1il35ee8gil43it4e8q7nl8jbrf8.apps.googleusercontent.com", 60 | "client_type": 3 61 | } 62 | ] 63 | } 64 | } 65 | } 66 | ], 67 | "configuration_version": "1" 68 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/kiwi/kiwitalk/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.kiwi.kiwitalk", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/debug/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/debug/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/debug/res/layout/layout_map_fragment_dummy.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | 27 | 28 | 33 | 37 | 42 | 47 | 51 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/assets/map_style.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "elementType": "geometry", 4 | "stylers": [ 5 | { 6 | "color": "#ebe3cd" 7 | } 8 | ] 9 | }, 10 | { 11 | "elementType": "labels.text.fill", 12 | "stylers": [ 13 | { 14 | "color": "#523735" 15 | } 16 | ] 17 | }, 18 | { 19 | "elementType": "labels.text.stroke", 20 | "stylers": [ 21 | { 22 | "color": "#f5f1e6" 23 | } 24 | ] 25 | }, 26 | { 27 | "featureType": "administrative", 28 | "elementType": "geometry.stroke", 29 | "stylers": [ 30 | { 31 | "color": "#c9b2a6" 32 | } 33 | ] 34 | }, 35 | { 36 | "featureType": "administrative.land_parcel", 37 | "elementType": "geometry.stroke", 38 | "stylers": [ 39 | { 40 | "color": "#dcd2be" 41 | } 42 | ] 43 | }, 44 | { 45 | "featureType": "administrative.land_parcel", 46 | "elementType": "labels.text.fill", 47 | "stylers": [ 48 | { 49 | "color": "#ae9e90" 50 | } 51 | ] 52 | }, 53 | { 54 | "featureType": "landscape.natural", 55 | "elementType": "geometry", 56 | "stylers": [ 57 | { 58 | "color": "#dfd2ae" 59 | } 60 | ] 61 | }, 62 | { 63 | "featureType": "poi", 64 | "elementType": "geometry", 65 | "stylers": [ 66 | { 67 | "color": "#dfd2ae" 68 | } 69 | ] 70 | }, 71 | { 72 | "featureType": "poi", 73 | "elementType": "labels.text", 74 | "stylers": [ 75 | { 76 | "visibility": "off" 77 | } 78 | ] 79 | }, 80 | { 81 | "featureType": "poi", 82 | "elementType": "labels.text.fill", 83 | "stylers": [ 84 | { 85 | "color": "#93817c" 86 | } 87 | ] 88 | }, 89 | { 90 | "featureType": "poi.business", 91 | "stylers": [ 92 | { 93 | "visibility": "off" 94 | } 95 | ] 96 | }, 97 | { 98 | "featureType": "poi.park", 99 | "elementType": "geometry.fill", 100 | "stylers": [ 101 | { 102 | "color": "#a5b076" 103 | } 104 | ] 105 | }, 106 | { 107 | "featureType": "poi.park", 108 | "elementType": "labels.text.fill", 109 | "stylers": [ 110 | { 111 | "color": "#447530" 112 | } 113 | ] 114 | }, 115 | { 116 | "featureType": "road", 117 | "elementType": "geometry", 118 | "stylers": [ 119 | { 120 | "color": "#f5f1e6" 121 | } 122 | ] 123 | }, 124 | { 125 | "featureType": "road", 126 | "elementType": "labels.icon", 127 | "stylers": [ 128 | { 129 | "visibility": "off" 130 | } 131 | ] 132 | }, 133 | { 134 | "featureType": "road.arterial", 135 | "elementType": "geometry", 136 | "stylers": [ 137 | { 138 | "color": "#fdfcf8" 139 | } 140 | ] 141 | }, 142 | { 143 | "featureType": "road.highway", 144 | "elementType": "geometry", 145 | "stylers": [ 146 | { 147 | "color": "#f8c967" 148 | } 149 | ] 150 | }, 151 | { 152 | "featureType": "road.highway", 153 | "elementType": "geometry.stroke", 154 | "stylers": [ 155 | { 156 | "color": "#e9bc62" 157 | } 158 | ] 159 | }, 160 | { 161 | "featureType": "road.highway.controlled_access", 162 | "elementType": "geometry", 163 | "stylers": [ 164 | { 165 | "color": "#e98d58" 166 | } 167 | ] 168 | }, 169 | { 170 | "featureType": "road.highway.controlled_access", 171 | "elementType": "geometry.stroke", 172 | "stylers": [ 173 | { 174 | "color": "#db8555" 175 | } 176 | ] 177 | }, 178 | { 179 | "featureType": "road.local", 180 | "elementType": "labels.text.fill", 181 | "stylers": [ 182 | { 183 | "color": "#806b63" 184 | } 185 | ] 186 | }, 187 | { 188 | "featureType": "transit", 189 | "stylers": [ 190 | { 191 | "visibility": "off" 192 | } 193 | ] 194 | }, 195 | { 196 | "featureType": "transit.line", 197 | "elementType": "geometry", 198 | "stylers": [ 199 | { 200 | "color": "#dfd2ae" 201 | } 202 | ] 203 | }, 204 | { 205 | "featureType": "transit.line", 206 | "elementType": "labels.text.fill", 207 | "stylers": [ 208 | { 209 | "color": "#8f7d77" 210 | } 211 | ] 212 | }, 213 | { 214 | "featureType": "transit.line", 215 | "elementType": "labels.text.stroke", 216 | "stylers": [ 217 | { 218 | "color": "#ebe3cd" 219 | } 220 | ] 221 | }, 222 | { 223 | "featureType": "transit.station", 224 | "elementType": "geometry", 225 | "stylers": [ 226 | { 227 | "color": "#dfd2ae" 228 | } 229 | ] 230 | }, 231 | { 232 | "featureType": "water", 233 | "elementType": "geometry.fill", 234 | "stylers": [ 235 | { 236 | "color": "#b9d3c2" 237 | } 238 | ] 239 | }, 240 | { 241 | "featureType": "water", 242 | "elementType": "labels.text.fill", 243 | "stylers": [ 244 | { 245 | "color": "#92998d" 246 | } 247 | ] 248 | } 249 | ] -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/KiwiApplication.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class KiwiApplication : Application() { 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/NetworkStateManager.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.ConnectivityManager 7 | import android.net.Network 8 | import android.net.NetworkCapabilities 9 | import android.net.NetworkRequest 10 | 11 | 12 | 13 | class NetworkStateManager(context: Context) : ConnectivityManager.NetworkCallback() { 14 | 15 | private var context : Context? = null 16 | private var networkRequest: NetworkRequest? = null 17 | private var connectivityManager : ConnectivityManager? = null 18 | private val msgBuilder: AlertDialog.Builder 19 | private val msgDlg: AlertDialog 20 | 21 | init { 22 | this.context = context 23 | this.networkRequest = 24 | NetworkRequest.Builder() 25 | .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) 26 | .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build() 27 | this.msgBuilder = AlertDialog.Builder(context) 28 | .setTitle("Network State Error") 29 | .setMessage("네트워크 상태를 확인해주세요.") 30 | .setPositiveButton("취소") { _, _ -> 31 | } 32 | .setCancelable(false) 33 | this.msgDlg = msgBuilder.create() 34 | } 35 | 36 | fun register() { 37 | connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 38 | connectivityManager?.registerNetworkCallback(networkRequest?:return, this) 39 | } 40 | 41 | fun unregister() { 42 | connectivityManager?.unregisterNetworkCallback(this) 43 | msgDlg.dismiss() 44 | } 45 | 46 | private fun networkShowDialog() { 47 | msgDlg.show() 48 | } 49 | 50 | override fun onLost(network: Network) { 51 | super.onLost(network) 52 | networkShowDialog() 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/di/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.di 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.os.Build 6 | import androidx.annotation.RequiresApi 7 | import com.google.android.gms.auth.api.signin.GoogleSignIn 8 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 9 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 10 | import com.kiwi.data.AppPreference 11 | import com.kiwi.kiwitalk.util.Const 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.components.SingletonComponent 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object ApplicationModule { 22 | @Singleton 23 | @Provides 24 | fun providePreference(@ApplicationContext context: Context): AppPreference = 25 | AppPreference(context.getSharedPreferences(Const.LOGIN_ID_KEY, Context.MODE_PRIVATE)) 26 | 27 | @Singleton 28 | @Provides 29 | fun provideGoogleApiClient(@ApplicationContext context: Context): GoogleSignInClient { 30 | val googleSignOptions: GoogleSignInOptions = 31 | GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) 32 | .requestIdToken("611516619499-v3b7482t6qbldpn0ens6rgp45i6fo577.apps.googleusercontent.com") 33 | .requestEmail() 34 | .build() 35 | return GoogleSignIn.getClient(context, googleSignOptions) 36 | } 37 | 38 | @RequiresApi(Build.VERSION_CODES.M) 39 | @Singleton 40 | @Provides 41 | fun provideConnectivityManager(@ApplicationContext context: Context): ConnectivityManager = 42 | context.getSystemService(ConnectivityManager::class.java) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/di/ChatModule.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import io.getstream.chat.android.client.ChatClient 10 | import io.getstream.chat.android.client.logger.ChatLogLevel 11 | import io.getstream.chat.android.offline.plugin.configuration.Config 12 | import io.getstream.chat.android.offline.plugin.factory.StreamOfflinePluginFactory 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | class ChatModule { 18 | @Provides 19 | @Singleton 20 | fun provideChatClient(@ApplicationContext context: Context): ChatClient { 21 | return ChatClient.Builder("b5m96pppmwh5", context) 22 | .withPlugin(getOfflinePlugin(context)) 23 | .logLevel(ChatLogLevel.ALL) 24 | .build() 25 | } 26 | 27 | @Singleton 28 | private fun getOfflinePlugin(@ApplicationContext context: Context) = 29 | StreamOfflinePluginFactory( 30 | config = Config(), 31 | appContext = context, 32 | ) 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/di/FirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.di 2 | 3 | import com.google.firebase.firestore.FirebaseFirestore 4 | import com.google.firebase.firestore.ktx.firestore 5 | import com.google.firebase.ktx.Firebase 6 | import com.google.firebase.storage.FirebaseStorage 7 | import com.google.firebase.storage.ktx.storage 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | class FirebaseModule { 17 | @Provides 18 | @Singleton 19 | fun provideFireStore(): FirebaseFirestore { 20 | return Firebase.firestore 21 | } 22 | 23 | @Provides 24 | @Singleton 25 | fun provideFireStorage(): FirebaseStorage { 26 | return Firebase.storage 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/di/KakaoApiModule.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.di 2 | 3 | import android.util.Log 4 | import com.kiwi.kiwitalk.util.Const.BASE_URL 5 | import com.kiwi.kiwitalk.util.Const.KAKAO_AUTH 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import okhttp3.Interceptor 11 | import okhttp3.OkHttpClient 12 | import okhttp3.Response 13 | import okhttp3.logging.HttpLoggingInterceptor 14 | import retrofit2.Retrofit 15 | import retrofit2.converter.gson.GsonConverterFactory 16 | import java.io.IOException 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object KakaoApiModule { 22 | 23 | @Provides 24 | @Singleton 25 | fun provideHttpLoggingInterceptor() = HttpLoggingInterceptor { message -> 26 | Log.d("API", message) 27 | }.apply { 28 | setLevel(HttpLoggingInterceptor.Level.BODY) 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | fun provideKakaoApiInterceptor(): ApiInterceptor = ApiInterceptor() 34 | 35 | @Provides 36 | @Singleton 37 | fun provideOkHttpClient( 38 | httpLoggingInterceptor: HttpLoggingInterceptor, 39 | apiInterceptor: ApiInterceptor, 40 | ): OkHttpClient = OkHttpClient.Builder() 41 | .addInterceptor(apiInterceptor) 42 | .addInterceptor(httpLoggingInterceptor) 43 | .build() 44 | 45 | @Provides 46 | @Singleton 47 | fun getClient( 48 | client: OkHttpClient, 49 | ): Retrofit = Retrofit.Builder() 50 | .baseUrl(BASE_URL) 51 | .addConverterFactory(GsonConverterFactory.create()) 52 | .client(client) 53 | .build() 54 | 55 | class ApiInterceptor : Interceptor { 56 | @Throws(IOException::class) 57 | override fun intercept(chain: Interceptor.Chain): Response { 58 | val request = chain.request().newBuilder() 59 | .addHeader(KAKAO_AUTH, "KakaoAK c42505e275dcf6c9770aa45d2db6cc49") 60 | .build() 61 | return chain.proceed(request) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.di 2 | 3 | import com.kiwi.data.datasource.local.UserLocalDataSource 4 | import com.kiwi.data.datasource.local.UserLocalDataSourceImpl 5 | import com.kiwi.data.datasource.remote.* 6 | import com.kiwi.data.repository.* 7 | import com.kiwi.domain.repository.* 8 | import dagger.Binds 9 | import dagger.Module 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | abstract class RepositoryModule { 17 | @Singleton 18 | @Binds //Interface 주입, abstract 사용 19 | abstract fun bindsSearchChatRepository(repository: SearchChatRepositoryImpl): SearchChatRepository 20 | 21 | @Singleton 22 | @Binds 23 | abstract fun bindsSearchChatRemoteDataSource(dataSource: SearchChatRemoteDataSourceImpl): SearchChatRemoteDataSource 24 | 25 | @Binds 26 | @Singleton 27 | abstract fun bindsSearchKeywordRepository(searchKeywordRepositoryImpl: SearchKeywordRepositoryImpl): SearchKeywordRepository 28 | 29 | @Binds 30 | @Singleton 31 | abstract fun bindsSearchPlaceRepository(searchPlaceRepositoryImpl: SearchPlaceRepositoryImpl): SearchPlaceRepository 32 | 33 | @Binds 34 | @Singleton 35 | abstract fun bindsSearchPlaceRemoteDataSource(searchPlaceRemoteDataSourceImpl: SearchPlaceRemoteDataSourceImpl): SearchPlaceRemoteDataSource 36 | 37 | @Binds 38 | @Singleton 39 | abstract fun bindsNewChatRemoteDataSource(newChatRemoteDataSourceImpl: NewChatRemoteDataSourceImpl): NewChatRemoteDataSource 40 | 41 | @Binds 42 | @Singleton 43 | abstract fun bindsNewChatRepository(newChatRepositoryImpl: NewChatRepositoryImpl): NewChatRepository 44 | 45 | @Binds 46 | @Singleton 47 | abstract fun bindsProfileSettingRepository(profileSettingRepositoryImpl: ProfileSettingRepositoryImpl): ProfileSettingRepository 48 | 49 | @Binds 50 | @Singleton 51 | abstract fun bindExitChatDataSource(exitChatRemoteDataSourceImpl: ExitChatRemoteDataSourceImpl): ExitChatRemoteDataSource 52 | 53 | @Binds 54 | @Singleton 55 | abstract fun bindExitChatRepository(exitChatRepositoryImpl: ExitChatRepositoryImpl): ExitChatRepository 56 | 57 | @Binds 58 | @Singleton 59 | abstract fun bindUserLocalDatasource(dataImpl: UserLocalDataSourceImpl): UserLocalDataSource 60 | 61 | @Binds 62 | @Singleton 63 | abstract fun bindUserRemoteDatasource(dataImpl: UserRemoteDataSourceImpl): UserRemoteDataSource 64 | 65 | @Binds 66 | @Singleton 67 | abstract fun bindUserRepository(repoImpl: UserRepositoryImpl): UserRepository 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/model/ClusterMarker.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.model 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import com.google.maps.android.clustering.ClusterItem 5 | import com.kiwi.domain.model.Marker 6 | 7 | data class ClusterMarker( 8 | val x: Double, 9 | val y: Double, 10 | private val markerSnippet: String?, 11 | private val markerTitle: String?, 12 | val keywords: List, 13 | val cid: String 14 | ) : ClusterItem { 15 | constructor( 16 | latLng: LatLng, snippet: String?, title: String?, keywords: List, cid: String 17 | ) : this(latLng.latitude, latLng.longitude, snippet, title, keywords, cid) 18 | 19 | private val latLng get() = LatLng(x, y) 20 | override fun getPosition(): LatLng = latLng 21 | override fun getSnippet(): String? = markerSnippet 22 | override fun getTitle(): String? = markerTitle 23 | 24 | companion object { 25 | fun Marker.toClusterMarker(): ClusterMarker = ClusterMarker( 26 | x = this.x, 27 | y = this.y, 28 | markerSnippet = cid, 29 | markerTitle = toString(), 30 | keywords = keywords, 31 | cid = cid 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/BindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import androidx.databinding.BindingAdapter 7 | import com.bumptech.glide.Glide 8 | import com.kiwi.kiwitalk.R 9 | import java.util.* 10 | 11 | @BindingAdapter("loadImageByUri") 12 | fun setImage(imageView: ImageView, uri: String?) { 13 | Glide.with(imageView.context) 14 | .load(uri) 15 | .placeholder(R.drawable.ic_baseline_cloud_sync_24) 16 | .error(R.drawable.logo_splash_light) 17 | .fitCenter() 18 | .into(imageView) 19 | } 20 | 21 | @BindingAdapter("setUnreadCount") 22 | fun setUnreadCount(textView: TextView, count: Int?) { 23 | when (count) { 24 | 0, null -> textView.visibility = View.INVISIBLE 25 | in 1..999 -> { 26 | textView.visibility = View.VISIBLE 27 | textView.text = count.toString() 28 | } 29 | else -> { 30 | textView.visibility = View.VISIBLE 31 | textView.text = textView.context.getText(R.string.tv_chatList_unreadCountMax) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/chatting/ChattingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.chatting 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import com.kiwi.kiwitalk.R 6 | import io.getstream.chat.android.ui.message.MessageListActivity 7 | import io.getstream.chat.android.ui.message.MessageListFragment 8 | 9 | class ChattingActivity : MessageListActivity() { 10 | override fun createMessageListFragment(cid: String, messageId: String?): MessageListFragment { 11 | return MessageListFragment.newInstance(cid) { 12 | setFragment(MessageListFragment()) 13 | customTheme(R.style.Theme_KiwiTalk_NoActionBar) 14 | showHeader(false) 15 | messageId(messageId) 16 | } 17 | } 18 | 19 | companion object { 20 | private const val EXTRA_CID: String = "extra_cid" 21 | private const val EXTRA_MESSAGE_ID: String = "extra_message_id" 22 | 23 | @JvmStatic 24 | @JvmOverloads 25 | fun createIntent(context: Context, cid: String, messageId: String? = null): Intent { 26 | return Intent(context, MessageListActivity::class.java).apply { 27 | putExtra(EXTRA_CID, cid) 28 | putExtra(EXTRA_MESSAGE_ID, messageId) 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/home/ChatListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.home 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.appcompat.app.AlertDialog 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.viewModels 12 | import androidx.navigation.Navigation 13 | import com.google.android.material.snackbar.Snackbar 14 | import com.kiwi.kiwitalk.R 15 | import com.kiwi.kiwitalk.databinding.FragmentChatListBinding 16 | import com.kiwi.kiwitalk.ui.chatting.ChattingActivity 17 | import com.kiwi.kiwitalk.ui.login.LoginActivity 18 | import com.kiwi.kiwitalk.ui.search.SearchChatActivity 19 | import dagger.hilt.android.AndroidEntryPoint 20 | import io.getstream.chat.android.client.models.Channel 21 | import io.getstream.chat.android.ui.channel.list.viewmodel.ChannelListViewModel 22 | import io.getstream.chat.android.ui.channel.list.viewmodel.factory.ChannelListViewModelFactory 23 | 24 | @AndroidEntryPoint 25 | class ChatListFragment : Fragment() { 26 | private val channelListViewModel: ChannelListViewModel 27 | by viewModels { ChannelListViewModelFactory() } 28 | private val chatListViewModel: ChatListViewModel by viewModels() 29 | 30 | private var _binding: FragmentChatListBinding? = null 31 | private val binding get() = _binding!! 32 | 33 | override fun onCreateView( 34 | inflater: LayoutInflater, container: ViewGroup?, 35 | savedInstanceState: Bundle? 36 | ): View { 37 | _binding = FragmentChatListBinding.inflate(layoutInflater, container, false) 38 | return binding.root 39 | } 40 | 41 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 42 | super.onViewCreated(view, savedInstanceState) 43 | initRecyclerView() 44 | initToolbar() 45 | 46 | binding.fabCreateChat.setOnClickListener { 47 | startActivity(Intent(requireContext(), SearchChatActivity::class.java)) 48 | } 49 | } 50 | 51 | private fun initRecyclerView() { 52 | val adapter = ChatListViewAdapter(object : ChatListViewAdapter.OnChatClickListener { 53 | override fun onChatClick(channel: Channel) { 54 | startActivity(ChattingActivity.createIntent(requireContext(), channel.cid)) 55 | } 56 | 57 | override fun onChatLongClick(channel: Channel) { 58 | showExitChatDialog(channel.cid) 59 | } 60 | }) 61 | 62 | channelListViewModel.state.observe(viewLifecycleOwner) { 63 | if (it.channels.isEmpty()) { 64 | binding.tvChatListEmpty.visibility = View.VISIBLE 65 | binding.rvChatList.visibility = View.INVISIBLE 66 | } else { 67 | binding.tvChatListEmpty.visibility = View.INVISIBLE 68 | binding.rvChatList.visibility = View.VISIBLE 69 | adapter.submitList(it.channels) 70 | } 71 | } 72 | binding.rvChatList.adapter = adapter 73 | } 74 | 75 | private fun initToolbar() { 76 | val navigation = Navigation.findNavController(binding.root) 77 | binding.chatListToolbar.setOnMenuItemClickListener { 78 | when (it.itemId) { 79 | R.id.item_chatList_logout -> { 80 | chatListViewModel.signOut() 81 | true 82 | } 83 | R.id.item_chatList_actionToProffileSetting -> { 84 | navigation.navigate(R.id.action_chatListFragment_to_profileSettingFragment) 85 | true 86 | } 87 | else -> false 88 | } 89 | } 90 | 91 | chatListViewModel.signOutState.observe(viewLifecycleOwner) { result -> 92 | Log.d("K001|ChatListFrag", "result : $result") 93 | if (result == true) { 94 | navigateToLogin() 95 | } else { 96 | Snackbar.make( 97 | requireContext(), 98 | requireView(), 99 | "로그아웃에 실패했습니다", 100 | Snackbar.LENGTH_SHORT 101 | ).show() 102 | } 103 | } 104 | } 105 | 106 | override fun onDestroyView() { 107 | super.onDestroyView() 108 | _binding = null 109 | } 110 | 111 | private fun showExitChatDialog(cid: String) { 112 | AlertDialog.Builder(requireContext()) 113 | .setTitle("채팅방 나가기") 114 | .setMessage("채팅방을 나가시겠습니까?") 115 | .setPositiveButton("확인") { _, _ -> 116 | chatListViewModel.exitChat(cid) 117 | } 118 | .setNegativeButton("취소") { dialog, _ -> 119 | dialog.dismiss() 120 | }.create() 121 | .show() 122 | } 123 | 124 | private fun navigateToLogin() { 125 | activity?.finishAffinity() 126 | startActivity(Intent(requireContext(), LoginActivity::class.java)) 127 | } 128 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/home/ChatListViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.home 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.kiwi.chatmapper.ChatMapper 9 | import com.kiwi.domain.model.Keyword 10 | import com.kiwi.kiwitalk.databinding.ItemChatListBinding 11 | import com.kiwi.kiwitalk.ui.keyword.recyclerview.SelectedKeywordAdapter 12 | import io.getstream.chat.android.client.models.Channel 13 | 14 | class ChatListViewAdapter(private val onClickListener: OnChatClickListener) : 15 | ListAdapter(ChatDiffUtil) { 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 18 | val binding = ItemChatListBinding.inflate( 19 | LayoutInflater.from(parent.context), parent, false 20 | ) 21 | val holder = ChatViewHolder(binding) 22 | binding.root.setOnClickListener { 23 | val position = holder.bindingAdapterPosition 24 | if (position != RecyclerView.NO_POSITION) { 25 | onClickListener.onChatClick(getItem(position)) 26 | } 27 | } 28 | binding.root.setOnLongClickListener { 29 | val position = holder.bindingAdapterPosition 30 | if (position != RecyclerView.NO_POSITION) { 31 | onClickListener.onChatLongClick(getItem(position)) 32 | } 33 | true 34 | } 35 | return holder 36 | } 37 | 38 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 39 | when (holder) { 40 | is ChatViewHolder -> holder.bind(getItem(position)) 41 | } 42 | } 43 | 44 | class ChatViewHolder(private val binding: ItemChatListBinding) : 45 | RecyclerView.ViewHolder(binding.root) { 46 | fun bind(item: Channel) { 47 | binding.chat = item 48 | val keywordAdapter = SelectedKeywordAdapter() 49 | val keywords = ChatMapper.getKeywords(item.extraData) 50 | keywordAdapter.submitList(keywords.map { Keyword(it, 0) }) 51 | binding.tvRvChatListKeyword.adapter = keywordAdapter 52 | } 53 | } 54 | 55 | interface OnChatClickListener { 56 | fun onChatClick(channel: Channel) 57 | 58 | fun onChatLongClick(channel: Channel) 59 | } 60 | 61 | companion object { 62 | val ChatDiffUtil = object : DiffUtil.ItemCallback() { 63 | override fun areItemsTheSame(oldItem: Channel, newItem: Channel): Boolean { 64 | return oldItem.cid == newItem.cid 65 | } 66 | 67 | override fun areContentsTheSame(oldItem: Channel, newItem: Channel): Boolean { 68 | return oldItem.cid == newItem.cid && oldItem.updatedAt == newItem.updatedAt && 69 | (oldItem.unreadCount == newItem.unreadCount || newItem.unreadCount == null || oldItem.unreadCount == null) && 70 | oldItem.memberCount == newItem.memberCount 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/home/ChatListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.home 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.kiwi.domain.repository.ExitChatRepository 8 | import com.kiwi.domain.repository.UserRepository 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.catch 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class ChatListViewModel @Inject constructor( 16 | private val exitChatRepository: ExitChatRepository, 17 | private val userRepository: UserRepository, 18 | ) : ViewModel() { 19 | private val TAG = "K001|ChatListViewModel" 20 | 21 | private val _signOutState = MutableLiveData() 22 | val signOutState: LiveData 23 | get() = _signOutState 24 | 25 | fun exitChat(cid: String) { 26 | viewModelScope.launch { 27 | exitChatRepository.exitChat(cid) 28 | } 29 | } 30 | 31 | fun signOut() { 32 | viewModelScope.launch { 33 | userRepository.signOut().catch { 34 | }.collect { 35 | _signOutState.postValue(it) 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/home/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.home 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.kiwi.kiwitalk.R 6 | import dagger.hilt.android.AndroidEntryPoint 7 | 8 | @AndroidEntryPoint 9 | class HomeActivity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_home) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/home/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.home 2 | 3 | import android.net.Uri 4 | import android.util.Log 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.ViewModel 8 | import com.google.firebase.ktx.Firebase 9 | import com.google.firebase.storage.ktx.storage 10 | import com.kiwi.domain.model.Keyword 11 | import com.kiwi.domain.model.UserInfo 12 | import com.kiwi.domain.repository.UserRepository 13 | import com.kiwi.kiwitalk.util.Const 14 | import com.kiwi.kiwitalk.util.Event 15 | import dagger.hilt.android.lifecycle.HiltViewModel 16 | import io.getstream.chat.android.client.ChatClient 17 | import io.getstream.chat.android.client.models.User 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class ProfileViewModel @Inject constructor( 22 | private val userRepository: UserRepository, 23 | ) : ViewModel() { 24 | 25 | //2way binding 26 | val myName = MutableLiveData() 27 | val myDescription = MutableLiveData() 28 | 29 | private val _profileImage = MutableLiveData() 30 | val profileImage: LiveData = _profileImage 31 | 32 | private val _myKeywords = MutableLiveData>() 33 | val myKeywords: LiveData> 34 | get() = _myKeywords 35 | 36 | private val _isAllConditionPass = MutableLiveData>() 37 | val isAllConditionPass: LiveData> 38 | get() = _isAllConditionPass 39 | 40 | init { 41 | getMyProfile() 42 | } 43 | 44 | private fun getMyProfile() { 45 | userRepository.getUserInfo().let { userInfo -> 46 | myName.value = userInfo.name 47 | myDescription.value = userInfo.description 48 | _myKeywords.value = userInfo.keywords 49 | _profileImage.value = userInfo.imageUrl.ifBlank { null } 50 | } 51 | } 52 | 53 | fun setMySelectedKeyword(selectKeywordList: List?) { 54 | selectKeywordList?.let { 55 | _myKeywords.value = it 56 | } 57 | } 58 | 59 | fun setUpdateProfile() { 60 | val uri = profileImage.value 61 | val id = userRepository.getUserInfo().id 62 | if (uri == null || uri.startsWith("https://")) { 63 | updateUser( 64 | UserInfo( 65 | id = id, 66 | name = myName.value ?: Const.EMPTY_STRING, 67 | keywords = myKeywords.value ?: listOf(), 68 | imageUrl = uri ?: Const.EMPTY_STRING, 69 | description = myDescription.value ?: Const.EMPTY_STRING 70 | ) 71 | ) 72 | } else { 73 | val ref = Firebase.storage.reference.child("profile/${id}") 74 | ref.putFile(Uri.parse(uri)).addOnSuccessListener { 75 | it.storage.downloadUrl.addOnCompleteListener { url -> 76 | updateUser( 77 | UserInfo( 78 | id = id, 79 | name = myName.value ?: Const.EMPTY_STRING, 80 | keywords = myKeywords.value ?: listOf(), 81 | imageUrl = url.result.toString(), 82 | description = myDescription.value ?: Const.EMPTY_STRING 83 | ) 84 | ) 85 | } 86 | }.addOnFailureListener { 87 | Log.d("NewChatDataSource", "putFile Failure: ${it.cause}") 88 | } 89 | } 90 | } 91 | 92 | private fun updateUser(userInfo: UserInfo) { 93 | userRepository.updateUser(userInfo) 94 | } 95 | 96 | fun setChatImage(uri: String) { 97 | _profileImage.value = uri 98 | } 99 | 100 | fun checkInput() { 101 | _isAllConditionPass.value = Event(checkName()) 102 | } 103 | 104 | private fun checkName(): Boolean { 105 | val nameStrng = myName.value 106 | return nameStrng?.contains(Regex("[^가-힣a-zA-Z]"))?.not() ?: false 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/keyword/SearchKeywordViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.keyword 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.kiwi.domain.model.Keyword 9 | import com.kiwi.domain.model.KeywordCategory 10 | import com.kiwi.domain.repository.SearchKeywordRepository 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class SearchKeywordViewModel @Inject constructor( 17 | private val searchKeywordRepository: SearchKeywordRepository 18 | ) : ViewModel() { 19 | 20 | private val _allKeywords = MutableLiveData>() 21 | val allKeywords: LiveData> 22 | get() = _allKeywords 23 | 24 | 25 | private val _selectedKeywords = MutableLiveData>() 26 | val selectedKeyword: LiveData> 27 | get() = _selectedKeywords 28 | 29 | private var tempSelectedKeywords: List? = null 30 | 31 | init { 32 | getAllKeywords() 33 | } 34 | 35 | private fun getAllKeywords() { 36 | viewModelScope.launch { 37 | allKeywords.value ?: searchKeywordRepository.getAllKeyWord() 38 | .let { 39 | _allKeywords.value = it 40 | Log.d("FIRESTORE_CALL_KEYWORD", "getKeywords: VM 1 ${it}") 41 | } 42 | } 43 | Log.d("FIRESTORE_CALL_KEYWORD", "getKeywords: VM 2") 44 | } 45 | 46 | fun deleteAllSelectedKeyword(){ 47 | _selectedKeywords.value = listOf() 48 | } 49 | 50 | fun setSelectedKeywords(text: String) { 51 | Keyword(text).let { keyword -> 52 | if (_selectedKeywords.value == null) { 53 | _selectedKeywords.value = listOf(keyword) 54 | } else { 55 | val mutableSelectedList = _selectedKeywords.value!!.toMutableList() 56 | if (mutableSelectedList.contains(keyword)) mutableSelectedList.remove(keyword) 57 | else mutableSelectedList.add(keyword) 58 | _selectedKeywords.value = mutableSelectedList 59 | } 60 | } 61 | } 62 | 63 | private fun getKeyword(text: String): Keyword? { 64 | allKeywords.value?.forEach { keywordCategory -> 65 | keywordCategory.keywords.forEach { keyword -> 66 | if (keyword.name == text) return keyword 67 | } 68 | } 69 | return null 70 | } 71 | 72 | fun saveBeforeKeywords() { 73 | selectedKeyword.value?.let { 74 | tempSelectedKeywords = it.toMutableList() 75 | } 76 | } 77 | 78 | fun SaveSelectedKeywordOrNot(saveOrNot: Boolean) { 79 | Log.d( 80 | "SaveSelected", 81 | "SaveSelectedKeywordOrNot: ${saveOrNot} before: $tempSelectedKeywords" 82 | ) 83 | if (!saveOrNot) { 84 | _selectedKeywords.value = tempSelectedKeywords ?: listOf() 85 | } 86 | tempSelectedKeywords = null 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/keyword/recyclerview/KeywordAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.keyword.recyclerview 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.kiwi.domain.model.Keyword 8 | import com.kiwi.kiwitalk.databinding.ItemKeywordBinding 9 | 10 | /** 11 | * TODO innerClass 없애기 12 | */ 13 | 14 | class KeywordAdapter( 15 | val keywordList: MutableList, 16 | private val keywordClickListener: (View) -> Unit, 17 | private val selectedKeywordList: List? = null 18 | ) : RecyclerView.Adapter() { 19 | 20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KeywordViewHolder { 21 | val binding = ItemKeywordBinding.inflate(LayoutInflater.from(parent.context)) 22 | return KeywordViewHolder(binding) 23 | } 24 | 25 | override fun onBindViewHolder(holder: KeywordViewHolder, position: Int) { 26 | holder.bind(position) 27 | } 28 | 29 | override fun getItemCount(): Int { 30 | return keywordList.size 31 | } 32 | 33 | //TODO 재활용을 위해 일반 클래스로 빼내기 34 | inner class KeywordViewHolder(val binding: ItemKeywordBinding) : 35 | RecyclerView.ViewHolder(binding.root) { 36 | fun bind(position: Int) { 37 | with(binding.chipKeyword) { 38 | text = keywordList[position].name 39 | setOnClickListener(keywordClickListener) 40 | selectedKeywordList?.let { list -> 41 | if (list.contains(keywordList[position])) this.isChecked = true 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/keyword/recyclerview/KeywordCategoryAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.keyword.recyclerview 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.google.android.flexbox.FlexboxLayoutManager 9 | import com.kiwi.domain.model.Keyword 10 | import com.kiwi.domain.model.KeywordCategory 11 | import com.kiwi.kiwitalk.databinding.ItemKeywordcategoryBinding 12 | 13 | class KeywordCategoryAdapter( 14 | var keywordCategoryList: List?, 15 | private val keywordClickListener: (View) -> Unit, 16 | private val selectedKeywordList: List? = null 17 | ) : RecyclerView.Adapter() { 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KeywordCategoryViewHolder { 20 | val binding = ItemKeywordcategoryBinding.inflate(LayoutInflater.from(parent.context)) 21 | return KeywordCategoryViewHolder(binding, parent.context) 22 | } 23 | 24 | override fun onBindViewHolder(holder: KeywordCategoryViewHolder, position: Int) { 25 | keywordCategoryList?.let { 26 | holder.bind(it[position].keywords, position) 27 | } 28 | } 29 | 30 | override fun getItemCount(): Int { 31 | return if (keywordCategoryList == null) 0 else keywordCategoryList!!.size 32 | } 33 | 34 | inner class KeywordCategoryViewHolder( 35 | val binding: ItemKeywordcategoryBinding, 36 | val contextForLayoutManager: Context, 37 | ) : RecyclerView.ViewHolder(binding.root) { 38 | fun bind(keywordList: MutableList, position: Int) { 39 | keywordCategoryList?.let { 40 | binding.tvKeywordCategoryCategoryName.text = it[position].name 41 | binding.rvKeywordCategoryKeywordList.layoutManager = 42 | FlexboxLayoutManager(contextForLayoutManager).apply { 43 | 44 | } 45 | binding.rvKeywordCategoryKeywordList.adapter = 46 | KeywordAdapter( 47 | it[position].keywords, 48 | keywordClickListener, 49 | selectedKeywordList 50 | ) 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/keyword/recyclerview/SelectedKeywordAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.keyword.recyclerview 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.kiwi.domain.model.Keyword 9 | import com.kiwi.kiwitalk.databinding.ItemKeywordBinding 10 | 11 | class SelectedKeywordAdapter() : 12 | ListAdapter(diffUtil) { 13 | 14 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectedKeywordViewHolder { 15 | val binding = ItemKeywordBinding.inflate(LayoutInflater.from(parent.context), parent, false) 16 | return SelectedKeywordViewHolder(binding) 17 | } 18 | 19 | override fun onBindViewHolder(holder: SelectedKeywordViewHolder, position: Int) { 20 | holder.bind(currentList[position]) 21 | } 22 | 23 | inner class SelectedKeywordViewHolder(val binding: ItemKeywordBinding) : 24 | RecyclerView.ViewHolder(binding.root) { 25 | fun bind(keyword: Keyword) { 26 | with(binding.chipKeyword) { 27 | text = keyword.name 28 | isChecked = true 29 | isClickable = false 30 | } 31 | } 32 | } 33 | 34 | companion object { 35 | val diffUtil = object : DiffUtil.ItemCallback() { 36 | override fun areItemsTheSame( 37 | oldItem: Keyword, 38 | newItem: Keyword 39 | ): Boolean { 40 | return oldItem.name == newItem.name 41 | } 42 | 43 | override fun areContentsTheSame( 44 | oldItem: Keyword, 45 | newItem: Keyword 46 | ): Boolean { 47 | return oldItem == newItem 48 | } 49 | } 50 | } 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/login/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.login 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.activity.result.ActivityResultLauncher 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.activity.viewModels 10 | import androidx.annotation.RequiresApi 11 | import androidx.appcompat.app.AppCompatActivity 12 | import com.google.android.gms.auth.api.Auth 13 | import com.google.android.gms.auth.api.signin.GoogleSignInResult 14 | import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes 15 | import com.google.android.material.snackbar.Snackbar 16 | import com.kiwi.kiwitalk.databinding.ActivityLoginBinding 17 | import com.kiwi.kiwitalk.ui.home.HomeActivity 18 | import com.kiwi.kiwitalk.util.Const 19 | import dagger.hilt.android.AndroidEntryPoint 20 | 21 | @AndroidEntryPoint 22 | class LoginActivity : AppCompatActivity() { 23 | private val viewModel: LoginViewModel by viewModels() 24 | private lateinit var binding: ActivityLoginBinding 25 | private lateinit var activityResultLauncher: ActivityResultLauncher 26 | private lateinit var loginProgressDialog: ProgressDialog 27 | 28 | @RequiresApi(Build.VERSION_CODES.S) 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | binding = ActivityLoginBinding.inflate(layoutInflater) 32 | setContentView(binding.root) 33 | 34 | viewModel.loginState.observe(this) { 35 | if (it) { 36 | showPopUpMessage(LOGIN_SUCCESS) 37 | navigateToHome() 38 | } 39 | } 40 | 41 | activityResultLauncher = 42 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 43 | loginWithGoogleCredential( 44 | Auth.GoogleSignInApi.getSignInResultFromIntent(it.data) 45 | ) 46 | } 47 | 48 | binding.btnGoogleSignup.setOnClickListener { 49 | val intent = viewModel.googleApiClient.signInIntent 50 | activityResultLauncher.launch(intent) 51 | } 52 | } 53 | 54 | @RequiresApi(Build.VERSION_CODES.M) 55 | private fun loginWithGoogleCredential(result: GoogleSignInResult?) { 56 | try { 57 | result ?: return 58 | Log.d(TAG, result.status.toString()) 59 | 60 | loginProgressDialog = 61 | ProgressDialog(this).apply { show() } 62 | 63 | when (result.status.statusCode) { 64 | GoogleSignInStatusCodes.SUCCESS -> result.signInAccount?.apply { 65 | viewModel.signUp( 66 | id = id!!, 67 | name = displayName ?: Const.EMPTY_STRING, 68 | imageUrl = photoUrl.toString() 69 | ) 70 | } 71 | GoogleSignInStatusCodes.DEVELOPER_ERROR -> throw Exception("SHA키 등록 여부 확인") 72 | GoogleSignInStatusCodes.NETWORK_ERROR -> showPopUpMessage(NO_NETWORK) 73 | 12501 -> throw Exception("디바이스가 Google Play 서비스를 포함하는지 확인") 74 | } 75 | loginProgressDialog.cancel() 76 | } catch (e: Exception) { 77 | Log.d(TAG, e.toString()) 78 | } 79 | } 80 | 81 | private fun navigateToHome() { 82 | val intent = Intent(this, HomeActivity::class.java) 83 | finishAffinity() 84 | startActivity(intent) 85 | } 86 | 87 | private fun showPopUpMessage(text: String) { 88 | Snackbar.make(binding.root, text, Snackbar.LENGTH_SHORT).show() 89 | } 90 | 91 | companion object { 92 | private const val TAG = "k001|LoginActivity" 93 | private const val LOGIN_SUCCESS = "로그인 성공" 94 | private const val NO_NETWORK = "인터넷 연결을 확인해주세요" 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.login 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 9 | import com.kiwi.domain.UserUiCallback 10 | import com.kiwi.domain.repository.UserRepository 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class LoginViewModel @Inject constructor( 17 | val googleApiClient: GoogleSignInClient, 18 | private val userRepository: UserRepository 19 | ) : ViewModel() { 20 | private val _loginState = MutableLiveData(false) 21 | val loginState: LiveData = _loginState 22 | 23 | fun signUp(id: String, name: String, imageUrl: String) { 24 | viewModelScope.launch { 25 | userRepository.tryLogin(id, name, imageUrl, object : UserUiCallback { 26 | override fun onSuccess() { 27 | _loginState.value = true 28 | } 29 | 30 | override fun onFailure(e: Throwable) { 31 | Log.d(TAG, "Login fail") 32 | e.printStackTrace() 33 | } 34 | }) 35 | } 36 | } 37 | 38 | companion object { 39 | private const val TAG = "k001|LoginVM" 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/login/ProgressDialog.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.login 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.os.Bundle 6 | import com.kiwi.kiwitalk.databinding.DialogLoginProgressBinding 7 | 8 | class ProgressDialog(context: Context) : Dialog(context) { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | val binding = DialogLoginProgressBinding.inflate(layoutInflater) 12 | setContentView(binding.root) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/login/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.login 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.activity.viewModels 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.kiwi.domain.UserUiCallback 10 | import com.kiwi.kiwitalk.NetworkStateManager 11 | import com.kiwi.kiwitalk.databinding.ActivitySplashBinding 12 | import com.kiwi.kiwitalk.ui.home.HomeActivity 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | @SuppressLint("CustomSplashScreen") 16 | @AndroidEntryPoint 17 | class SplashActivity : AppCompatActivity() { 18 | private val viewModel: SplashViewModel by viewModels() 19 | private lateinit var binding: ActivitySplashBinding 20 | private val networkStateManager by lazy { NetworkStateManager(this) } 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | binding = ActivitySplashBinding.inflate(layoutInflater) 25 | setContentView(binding.root) 26 | } 27 | 28 | override fun onStart() { 29 | super.onStart() 30 | networkStateManager.register() 31 | viewModel.loginWithLocalToken(object : UserUiCallback { 32 | override fun onSuccess() { 33 | navigateToHomeActivity() 34 | } 35 | 36 | override fun onFailure(e: Throwable) { 37 | Log.d("K001|Splash", e.toString()) 38 | navigateToLoginActivity() 39 | } 40 | }) 41 | } 42 | 43 | private fun navigateToLoginActivity() { 44 | finishAffinity() 45 | startActivity(Intent(this, LoginActivity::class.java)) 46 | } 47 | 48 | private fun navigateToHomeActivity() { 49 | finishAffinity() 50 | startActivity(Intent(this, HomeActivity::class.java)) 51 | } 52 | 53 | override fun onStop() { 54 | super.onStop() 55 | networkStateManager.unregister() 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/login/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.login 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.kiwi.domain.UserUiCallback 5 | import com.kiwi.domain.repository.UserRepository 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import javax.inject.Inject 8 | 9 | @HiltViewModel 10 | class SplashViewModel @Inject constructor( 11 | private val userRepository: UserRepository, 12 | ): ViewModel() { 13 | 14 | fun loginWithLocalToken(userUiCallback: UserUiCallback){ 15 | userRepository.isRemoteLoginRequired(userUiCallback) 16 | } 17 | 18 | companion object { 19 | private const val TAG = "k001|SplashVM" 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/newchat/NewChatActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.newchat 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.kiwi.kiwitalk.databinding.ActivityNewChatBinding 6 | import dagger.hilt.android.AndroidEntryPoint 7 | 8 | @AndroidEntryPoint 9 | class NewChatActivity : AppCompatActivity() { 10 | 11 | private lateinit var binding: ActivityNewChatBinding 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | binding = ActivityNewChatBinding.inflate(layoutInflater) 16 | setContentView(binding.root) 17 | 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/newchat/NewChatViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.newchat 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.google.android.gms.maps.model.LatLng 8 | import com.kiwi.domain.model.NewChatInfo 9 | import com.kiwi.domain.repository.NewChatRepository 10 | import com.kiwi.data.AppPreference 11 | import com.kiwi.domain.repository.UserRepository 12 | import com.kiwi.kiwitalk.util.Const.EMPTY_STRING 13 | import com.kiwi.kiwitalk.util.Const.LOGIN_ID_KEY 14 | import dagger.hilt.android.lifecycle.HiltViewModel 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class NewChatViewModel @Inject constructor( 20 | private val repository: NewChatRepository, 21 | private val userRepository: UserRepository, 22 | ) : ViewModel() { 23 | 24 | private val _chatImage = MutableLiveData() 25 | val chatImage: LiveData = _chatImage 26 | 27 | private val _chatId = MutableLiveData() 28 | val chatId: LiveData = _chatId 29 | 30 | private val _newChatInfo = MutableLiveData() 31 | val newChatInfo: LiveData = _newChatInfo 32 | 33 | private val _address = MutableLiveData() 34 | val address: LiveData = _address 35 | 36 | private val _latLng = MutableLiveData() 37 | val latLng: LiveData = _latLng 38 | 39 | 40 | fun setChatImage(uri: String) { 41 | _chatImage.value = uri 42 | } 43 | 44 | fun setAddress(address: String) { 45 | _address.value = address 46 | } 47 | 48 | fun setLatLng(latLng: LatLng) { 49 | _latLng.value = latLng 50 | } 51 | 52 | fun setNewChat(newChat: NewChatInfo) { 53 | _newChatInfo.value = newChat 54 | } 55 | 56 | fun setChatId() { 57 | _chatId.value = userRepository.getUserInfo().id 58 | } 59 | 60 | fun addNewChat(userid: String, currentTime: String, newChat: NewChatInfo) { 61 | viewModelScope.launch { 62 | repository.addChatUpload(userid,currentTime,newChat) 63 | } 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/newchat/SearchPlaceViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.newchat 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.kiwi.domain.model.PlaceInfoList 7 | import com.kiwi.domain.repository.SearchPlaceRepository 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import io.getstream.chat.android.client.utils.toResult 10 | import kotlinx.coroutines.flow.* 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class SearchPlaceViewModel @Inject constructor( 16 | private val searchPlaceRepository: SearchPlaceRepository 17 | ) : ViewModel() { 18 | 19 | private val _isPlaceList = MutableSharedFlow() 20 | val isPlaceList: SharedFlow = _isPlaceList 21 | 22 | fun getSearchPlace(lng: String, lat: String, place: String) { 23 | viewModelScope.launch { 24 | searchPlaceRepository.getSearchPlace(lng, lat, place) 25 | .catch { 26 | Log.d("getSearchPlace", "getSearchPlace: ${this.toResult()} $it") 27 | }.collect { 28 | _isPlaceList.emit(it) 29 | } 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/search/ChatAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.search 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.kiwi.domain.model.ChatInfo 7 | import com.kiwi.domain.model.Keyword 8 | import com.kiwi.kiwitalk.databinding.ItemChatListNoImageBinding 9 | import com.kiwi.kiwitalk.ui.keyword.recyclerview.SelectedKeywordAdapter 10 | 11 | class ChatAdapter( 12 | private var chatList: List?, 13 | private val chatClickListener: (ChatInfo) -> Unit 14 | ) : RecyclerView.Adapter() { 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatInfoViewHolder { 17 | val binding = ItemChatListNoImageBinding.inflate(LayoutInflater.from(parent.context)) 18 | 19 | return ChatInfoViewHolder(binding) 20 | } 21 | 22 | override fun onBindViewHolder(holder: ChatInfoViewHolder, position: Int) { 23 | chatList?.let { 24 | val data = it[position] 25 | holder.bind(data) 26 | holder.binding.root.setOnClickListener { 27 | chatClickListener(data) 28 | } 29 | } 30 | } 31 | 32 | override fun getItemCount(): Int { 33 | return chatList?.size ?: 0 34 | } 35 | 36 | fun submitList(list: List) { 37 | chatList = list 38 | notifyDataSetChanged() 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/search/ChatInfoViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.search 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import com.kiwi.domain.model.ChatInfo 5 | import com.kiwi.domain.model.Keyword 6 | import com.kiwi.kiwitalk.databinding.ItemChatListNoImageBinding 7 | import com.kiwi.kiwitalk.ui.keyword.recyclerview.SelectedKeywordAdapter 8 | 9 | class ChatInfoViewHolder( 10 | val binding: ItemChatListNoImageBinding 11 | ) : RecyclerView.ViewHolder(binding.root) { 12 | fun bind(chatInfo: ChatInfo) { 13 | binding.item = chatInfo 14 | val adapter = SelectedKeywordAdapter() 15 | binding.adapter = adapter 16 | adapter.submitList(chatInfo.keywords.map { 17 | Keyword(it, 0) 18 | }.toMutableList()) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/search/ChatJoinDialog.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.search 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.fragment.app.DialogFragment 9 | import com.kiwi.domain.model.ChatInfo 10 | import com.kiwi.domain.model.Keyword 11 | import com.kiwi.kiwitalk.R 12 | import com.kiwi.kiwitalk.databinding.DialogJoinChatBinding 13 | import com.kiwi.kiwitalk.ui.keyword.recyclerview.SelectedKeywordAdapter 14 | 15 | class ChatJoinDialog( 16 | private val chatDialogAction: ChatDialogAction, 17 | private val chatInfo: ChatInfo, 18 | ) : DialogFragment() { 19 | 20 | private var _binding: DialogJoinChatBinding? = null 21 | private val binding get() = _binding!! 22 | 23 | override fun onCreateView( 24 | inflater: LayoutInflater, 25 | container: ViewGroup?, 26 | savedInstanceState: Bundle? 27 | ): View { 28 | _binding = DataBindingUtil.inflate(inflater, R.layout.dialog_join_chat, container, false) 29 | binding.chatInfo = chatInfo 30 | binding.adapter = SelectedKeywordAdapter().apply { 31 | submitList(chatInfo.keywords.map { 32 | Keyword(it, 0) 33 | }.toMutableList()) 34 | } 35 | binding.btnDialogClose.setOnClickListener { 36 | chatDialogAction.onClickCancleButton() 37 | dismiss() 38 | } 39 | binding.btnDialogJoin.setOnClickListener { 40 | chatDialogAction.onClickJoinButton(chatInfo.cid) 41 | dismiss() 42 | } 43 | return binding.root 44 | } 45 | 46 | override fun onDestroy() { 47 | super.onDestroy() 48 | _binding = null 49 | } 50 | 51 | } 52 | 53 | interface ChatDialogAction { 54 | fun onClickJoinButton(cid: String) 55 | fun onClickCancleButton() 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/search/ClusterMarkerRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.search 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.widget.TextView 7 | import com.google.android.gms.maps.GoogleMap 8 | import com.google.android.gms.maps.model.BitmapDescriptorFactory 9 | import com.google.android.gms.maps.model.Marker 10 | import com.google.android.gms.maps.model.MarkerOptions 11 | import com.google.maps.android.clustering.Cluster 12 | import com.google.maps.android.clustering.ClusterManager 13 | import com.google.maps.android.clustering.view.DefaultClusterRenderer 14 | import com.kiwi.kiwitalk.R 15 | import com.kiwi.kiwitalk.model.ClusterMarker 16 | import com.kiwi.kiwitalk.util.Util.createDrawableFromView 17 | 18 | class ClusterMarkerRenderer(val context: Context, map: GoogleMap, clusterManager: ClusterManager) 19 | : DefaultClusterRenderer(context, map, clusterManager) { 20 | 21 | 22 | override fun onBeforeClusterItemRendered(item: ClusterMarker, markerOptions: MarkerOptions) { 23 | val view: View = LayoutInflater.from(context).inflate(R.layout.item_marker, null) 24 | markerOptions.icon(BitmapDescriptorFactory.fromBitmap(createDrawableFromView(view))) 25 | } 26 | 27 | override fun onClusterRendered(cluster: Cluster, marker: Marker) { 28 | val view: View = LayoutInflater.from(context).inflate(R.layout.item_marker, null) 29 | val markerCount = view.findViewById(R.id.tv_marker_count) 30 | markerCount.text = cluster.size.toString() 31 | markerCount.visibility = View.VISIBLE 32 | marker.setIcon(BitmapDescriptorFactory.fromBitmap(createDrawableFromView(view))) 33 | } 34 | 35 | override fun shouldRenderAsCluster(cluster: Cluster): Boolean { 36 | return cluster.size > 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/search/SearchChatActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.search 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.kiwi.kiwitalk.R 6 | import dagger.hilt.android.AndroidEntryPoint 7 | 8 | @AndroidEntryPoint 9 | class SearchChatActivity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_search_chat) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/ui/search/SearchChatMapViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.ui.search 2 | 3 | import android.location.Location 4 | import android.util.Log 5 | import androidx.core.location.component1 6 | import androidx.core.location.component2 7 | import androidx.lifecycle.LiveData 8 | import androidx.lifecycle.MutableLiveData 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import com.kiwi.domain.model.ChatInfo 12 | import com.kiwi.domain.model.Keyword 13 | import com.kiwi.domain.model.Marker 14 | import com.kiwi.domain.model.PlaceChatInfo 15 | import com.kiwi.domain.repository.SearchChatRepository 16 | import dagger.hilt.android.lifecycle.HiltViewModel 17 | import io.getstream.chat.android.client.utils.toResult 18 | import kotlinx.coroutines.flow.MutableSharedFlow 19 | import kotlinx.coroutines.flow.SharedFlow 20 | import kotlinx.coroutines.flow.catch 21 | import kotlinx.coroutines.flow.onCompletion 22 | import kotlinx.coroutines.launch 23 | import javax.inject.Inject 24 | 25 | @HiltViewModel 26 | class SearchChatMapViewModel @Inject constructor( 27 | private val searchChatRepository: SearchChatRepository, 28 | ) : ViewModel() { 29 | private val _markerList = MutableSharedFlow() 30 | val markerList: SharedFlow = _markerList 31 | 32 | private val _placeChatInfo = MutableLiveData() 33 | val placeChatInfo: LiveData = _placeChatInfo 34 | 35 | private val _location = MutableLiveData() 36 | val location: LiveData = _location 37 | 38 | private val _isLoadingMarkerList = MutableLiveData(false) 39 | val isLoadingMarkerList: LiveData 40 | get() = _isLoadingMarkerList 41 | 42 | var dialogData: ChatInfo? = null 43 | var lastBottomSheetState: Int? = null 44 | 45 | fun appendUserToChat(cid: String) { 46 | searchChatRepository.appendUserToChat(cid) 47 | } 48 | 49 | fun getPlaceInfo(cidList: List) { 50 | viewModelScope.launch { 51 | _placeChatInfo.value = searchChatRepository.getPlaceChatList(cidList) 52 | } 53 | } 54 | 55 | fun getMarkerList(keywords: List?) { 56 | if (keywords.isNullOrEmpty()) return 57 | val (lat, lng) = location.value ?: return 58 | _isLoadingMarkerList.value = true 59 | viewModelScope.launch { 60 | searchChatRepository.getMarkerList(keywords, lat, lng) 61 | .catch { 62 | Log.d("SearchChatViewModel", "getMarkerList: ${this.toResult()} $it") 63 | }.onCompletion { 64 | _isLoadingMarkerList.value = false 65 | }.collect { 66 | Log.d("SearchChatViewModel", "getMarkerList: collect $it") 67 | _markerList.emit(it) 68 | } 69 | } 70 | } 71 | 72 | fun setDeviceLocation(newLocation: Location) { 73 | _location.value = newLocation 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/util/ChangeExpansion.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.util 2 | 3 | import android.location.Geocoder 4 | 5 | object ChangeExpansion { 6 | fun Geocoder.changeLatLngToAddress( 7 | latitude: Double, 8 | longitude: Double, 9 | address: (android.location.Address?) -> Unit 10 | ) { 11 | try { 12 | @Suppress("DEPRECATION") 13 | address(getFromLocation(latitude, longitude, 1)?.firstOrNull()) 14 | } catch (e: Exception) { 15 | address(null) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/util/Const.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.util 2 | 3 | object Const { 4 | const val SPACE = " " 5 | const val LOGIN_ID_KEY = "login_history_key" 6 | const val LOGIN_NAME_KEY = "login_name" 7 | const val LOGIN_URL_KEY = "login_url" 8 | const val EMPTY_STRING = "" 9 | const val BASE_URL = "https://dapi.kakao.com/" 10 | const val KAKAO_AUTH = "Authorization" 11 | const val ADDRESS_ERROR = "현재 좌표에서 주소를 가져올 수 없습니다." 12 | const val PERMISSION_CODE = 99 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/util/Event.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.util 2 | 3 | /** 4 | * Code from : https://github.com/android/architecture-samples/ 5 | */ 6 | 7 | 8 | /** 9 | * Used as a wrapper for data that is exposed via a LiveData that represents an event. 10 | */ 11 | open class Event(private val content: T) { 12 | 13 | var hasBeenHandled = false 14 | private set // Allow external read but not write 15 | 16 | /** 17 | * Returns the content and prevents its use again. 18 | */ 19 | fun getContentIfNotHandled(): T? { 20 | return if (hasBeenHandled) { 21 | null 22 | } else { 23 | hasBeenHandled = true 24 | content 25 | } 26 | } 27 | 28 | /** 29 | * Returns the content, even if it's already been handled. 30 | */ 31 | fun peekContent(): T = content 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kiwi/kiwitalk/util/Util.kt: -------------------------------------------------------------------------------- 1 | package com.kiwi.kiwitalk.util 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.os.* 7 | import android.util.DisplayMetrics 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.core.content.ContextCompat 11 | import com.google.android.gms.maps.model.BitmapDescriptor 12 | import com.google.android.gms.maps.model.BitmapDescriptorFactory 13 | 14 | 15 | object Util { 16 | @Suppress("DEPRECATION") 17 | fun generateVibrator(context: Context) { 18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 19 | val vibratorManager = 20 | context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager 21 | 22 | val vibrationEffect = VibrationEffect.createOneShot( 23 | 200L, 24 | 50 25 | ) 26 | val combinedVibration = CombinedVibration.createParallel(vibrationEffect) 27 | vibratorManager.vibrate(combinedVibration) 28 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 29 | val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 30 | val effect = VibrationEffect.createOneShot( 31 | 200L, 50 32 | ) 33 | vibrator.vibrate(effect) 34 | } else { 35 | val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 36 | vibrator.vibrate(200L) 37 | } 38 | } 39 | 40 | fun changeVectorToBitmapDescriptor(context: Context, vectorResId: Int): BitmapDescriptor? { 41 | return ContextCompat.getDrawable(context, vectorResId)?.run { 42 | setBounds(0, 0, intrinsicWidth, intrinsicHeight) 43 | val bitmap = 44 | Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888) 45 | draw(Canvas(bitmap)) 46 | BitmapDescriptorFactory.fromBitmap(bitmap) 47 | } 48 | } 49 | 50 | fun createDrawableFromView(view: View): Bitmap { 51 | val displayMetrics = DisplayMetrics() 52 | view.layoutParams = ViewGroup.LayoutParams( 53 | ViewGroup.LayoutParams.WRAP_CONTENT, 54 | ViewGroup.LayoutParams.WRAP_CONTENT 55 | ) 56 | view.measure(displayMetrics.widthPixels, displayMetrics.heightPixels) 57 | view.layout(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels) 58 | val bitmap = Bitmap.createBitmap( 59 | view.measuredWidth, 60 | view.measuredHeight, 61 | Bitmap.Config.ARGB_8888 62 | ) 63 | val canvas = Canvas(bitmap) 64 | view.draw(canvas) 65 | return bitmap 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /app/src/main/res/color/color_chip_background_keyword.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/color_chip_text_keyword.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_marker_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_chat_list_unread_count.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_location.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_photo.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_backarrow_48.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_cloud_sync_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chatlist_searchchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/main/res/drawable/ic_chatlist_searchchat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location_cluster.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location_on_click.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_profile_base_image.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_chat_new_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/main/res/drawable/ic_search_chat_new_chat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo_splash_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/main/res/drawable/logo_splash_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo_splash_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/main/res/drawable/logo_splash_light.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo_splash_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/main/res/drawable/logo_splash_transparent.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo_splash_withtext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android08-KiwiTalk/f6c3c00bc398b4e35a555fb98d07d2998c1814eb/app/src/main/res/drawable/logo_splash_withtext.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_chatting.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_new_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_login_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_new_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 26 | 27 | 28 | 36 | 37 |