diff --git a/.github/workflows/android-pull-request-ci.yml b/.github/workflows/android-pull-request-ci.yml
index d012d14ae..68ecc0056 100644
--- a/.github/workflows/android-pull-request-ci.yml
+++ b/.github/workflows/android-pull-request-ci.yml
@@ -3,6 +3,8 @@ name: Android Pull Request CI
on:
push:
branches: [ develop ]
+ paths:
+ - 'android/**'
pull_request:
branches: [ develop ]
paths:
@@ -57,14 +59,36 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
+ - name: Create local.properties
+ env:
+ LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
+ run: |
+ echo "$LOCAL_PROPERTIES" > local.properties
+
+ - name: Create google-services.json
+ env:
+ GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
+ run: |
+ touch ../android/app/google-services.json
+ echo GOOGLE_SERVICES_JSON >> ../android/app/google-services.json
+ cat ../android/app/google-services.json
+
- name: Lint Check
run: ./gradlew ktlintCheck
- name: Upload Event File
uses: actions/upload-artifact@v3
with:
- name: Event File
- path: ${{ github.event_path }}
+ name: Event File
+ path: ${{ github.event_path }}
+
+ - name: Create file
+ run: cat /home/runner/work/2024-friendogly/2024-friendogly/android/app/google-services.json | base64
+
+ - name: Putting data
+ env:
+ DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
+ run: echo $DATA > /home/runner/work/2024-friendogly/2024-friendogly/android/app/google-services.json
- name: Run unit tests
run: ./gradlew testDebugUnitTest --stacktrace
@@ -73,5 +97,5 @@ jobs:
if: always()
uses: actions/upload-artifact@v3
with:
- name: Test Results
- path: "**/test-results/**/*.xml"
+ name: Test Results
+ path: "**/test-results/**/*.xml"
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index efa26dbc8..3e8564e3d 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,10 +1,22 @@
+import java.io.FileInputStream
+import java.util.Properties
+
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
- id("kotlin-kapt")
alias(libs.plugins.navigation.safeargs)
+ id("kotlin-kapt")
+ id("com.google.gms.google-services")
+ id("kotlin-parcelize")
+ kotlin("plugin.serialization")
}
+val localPropertiesFile = rootProject.file("local.properties")
+val localProperties = Properties()
+localProperties.load(FileInputStream(localPropertiesFile))
+
+val googleClientId = localProperties.getProperty("GOOGLE_CLIENT_ID") ?: ""
+
android {
namespace = "com.woowacourse.friendogly"
compileSdk = 34
@@ -17,6 +29,8 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ buildConfigField("String", "GOOGLE_CLIENT_ID", googleClientId)
}
buildTypes {
@@ -38,6 +52,9 @@ android {
dataBinding {
enable = true
}
+ buildFeatures {
+ buildConfig = true
+ }
}
dependencies {
@@ -52,4 +69,13 @@ dependencies {
testImplementation(libs.bundles.test)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
+
+ // retrofit
+ implementation("com.squareup.retrofit2:retrofit:2.11.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.11.0")
+ implementation("com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0")
+
+ // serialization
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
+
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1a873be88..abd8ef719 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -12,6 +12,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.Friendogly"
tools:targetApi="31">
+
@@ -24,6 +27,11 @@
+
+
+
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/ChatListFragment.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/ChatListFragment.kt
new file mode 100644
index 000000000..2a81dba14
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/ChatListFragment.kt
@@ -0,0 +1,22 @@
+package com.woowacourse.friendogly.presentation.ui.chatlist
+
+import com.woowacourse.friendogly.R
+import com.woowacourse.friendogly.databinding.FragmentChatListBinding
+import com.woowacourse.friendogly.presentation.base.BaseFragment
+
+class ChatListFragment : BaseFragment(R.layout.fragment_chat_list) {
+ override fun initViewCreated() {
+
+ binding.test.setOnClickListener {
+ val bottomSheet = WoofBottomSheet()
+ val bundle = WoofBottomSheet.getBundle(WoofDogUiModel("https://t1.daumcdn.net/thumb/R720x0.fjpg/?fname=http://t1.daumcdn.net/brunch/service/user/cnoC/image/PTcGsuuqjlyY1d9MxFkG7RAndmo.jpg",
+ "땡이","소형견",2,"땡이 닉네임이랑 땡이 강아지 이름 똑가틈"))
+ bottomSheet.arguments = bundle
+ bottomSheet.show(parentFragmentManager, "")
+ }
+ }
+
+
+
+
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/WoofBottomSheet.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/WoofBottomSheet.kt
new file mode 100644
index 000000000..74094947c
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/WoofBottomSheet.kt
@@ -0,0 +1,80 @@
+package com.woowacourse.friendogly.presentation.ui.chatlist
+
+import android.app.Dialog
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.woowacourse.friendogly.R
+import com.woowacourse.friendogly.presentation.utils.bindGlide1000
+import com.woowacourse.friendogly.presentation.utils.bundleParcelable
+
+class WoofBottomSheet : BottomSheetDialogFragment() {
+
+ private lateinit var dlg: BottomSheetDialog
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ dlg = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
+ dlg.setOnShowListener {
+ val bottomSheet =
+ dlg.findViewById(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout
+
+ val behavior = BottomSheetBehavior.from(bottomSheet)
+ behavior.isDraggable = false
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ return dlg
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.bottom_sheet_woof, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ //setBackgroundTransparent()
+
+ val info = this.arguments?.bundleParcelable(EXTRA_DOG_INFO_ID, WoofDogUiModel::class.java)
+ ?: error("dog info가 잘못 들어옴")
+
+ dlg.findViewById(R.id.tv_woof_dog_name)?.text = info.name
+ dlg.findViewById(R.id.tv_woof_dog_age)?.text = "${info.age} 살"
+ dlg.findViewById(R.id.tv_woof_dog_gender)?.text = info.name
+ dlg.findViewById(R.id.tv_woof_dog_size)?.text = info.size
+ dlg.findViewById(R.id.tv_woof_dog_desc)?.text = info.description
+
+ Glide.with(requireContext())
+ .load(info.imageUrl)
+ .into(dlg.findViewById(R.id.iv_woof_dog)!!)
+
+ }
+
+ private fun setBackgroundTransparent() {
+ dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ }
+
+ companion object {
+ private const val EXTRA_DOG_INFO_ID = "dogInfo"
+
+ fun getBundle(dog: WoofDogUiModel): Bundle {
+ return Bundle().apply { this.putParcelable(EXTRA_DOG_INFO_ID, dog) }
+ }
+ }
+
+
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/WoofDogUiModel.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/WoofDogUiModel.kt
new file mode 100644
index 000000000..1b541f77e
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/chatlist/WoofDogUiModel.kt
@@ -0,0 +1,14 @@
+package com.woowacourse.friendogly.presentation.ui.chatlist
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+
+@Parcelize
+data class WoofDogUiModel(
+ val imageUrl: String,
+ val name: String,
+ val size: String,
+ val age: Int,
+ val description: String,
+): Parcelable
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfieSettingBindingAdapters.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfieSettingBindingAdapters.kt
new file mode 100644
index 000000000..6a65ee30c
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfieSettingBindingAdapters.kt
@@ -0,0 +1,39 @@
+package com.woowacourse.friendogly.presentation.ui.profilesetting
+
+import android.annotation.SuppressLint
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.databinding.BindingAdapter
+import com.woowacourse.friendogly.R
+
+@SuppressLint("SetTextI18n")
+@BindingAdapter("editTextLength")
+fun TextView.bindEditTextLength(contents: String?) {
+ val length = contents?.length ?: 0
+
+ val color =
+ if (length != 0) {
+ ContextCompat.getColor(context, R.color.black)
+ } else {
+ ContextCompat.getColor(context, R.color.gray05)
+ }
+
+ this.apply {
+ text = "$length/15"
+ setTextColor(color)
+ }
+}
+
+@SuppressLint("UseCompatLoadingForDrawables")
+@BindingAdapter("editBtnBackgroundTextColor")
+fun TextView.bindEditBtnBackground(contents: String?) {
+ val length = contents?.length ?: 0
+
+ if (length > 0) {
+ this.background = context.getDrawable(R.drawable.rect_blue_fill_16)
+ this.setTextColor(context.getColor(R.color.black))
+ } else {
+ this.background = context.getDrawable(R.drawable.rect_gray03_fill_16)
+ this.setTextColor(context.getColor(R.color.gray08))
+ }
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingActivity.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingActivity.kt
new file mode 100644
index 000000000..a2980d076
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingActivity.kt
@@ -0,0 +1,122 @@
+package com.woowacourse.friendogly.presentation.ui.profilesetting
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.net.Uri
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import com.canhub.cropper.CropImageContract
+import com.canhub.cropper.CropImageContractOptions
+import com.canhub.cropper.CropImageOptions
+import com.woowacourse.friendogly.R
+import com.woowacourse.friendogly.databinding.ActivityProfileSettingBinding
+import com.woowacourse.friendogly.presentation.base.BaseActivity
+import com.woowacourse.friendogly.presentation.base.observeEvent
+import com.woowacourse.friendogly.presentation.ui.MainActivity
+import com.woowacourse.friendogly.presentation.ui.profilesetting.bottom.EditProfileImageBottomSheet
+import com.woowacourse.friendogly.presentation.utils.customOnFocusChangeListener
+import com.woowacourse.friendogly.presentation.utils.hideKeyboard
+import com.woowacourse.friendogly.presentation.utils.saveBitmapToFile
+import com.woowacourse.friendogly.presentation.utils.toBitmap
+import com.woowacourse.friendogly.presentation.utils.toMultipartBody
+
+class ProfileSettingActivity :
+ BaseActivity(R.layout.activity_profile_setting) {
+ private val viewModel: ProfileSettingViewModel by viewModels()
+
+ private lateinit var imagePickerLauncher: ActivityResultLauncher
+ private lateinit var imageCropLauncher: ActivityResultLauncher
+ private lateinit var cropImageOptions: CropImageOptions
+
+ override fun initCreateView() {
+ initDataBinding()
+ initObserve()
+ initImageLaunchers()
+ initEditText()
+ }
+
+ private fun initDataBinding() {
+ binding.vm = viewModel
+ }
+
+ private fun initObserve() {
+ viewModel.navigateAction.observeEvent(this) { action ->
+ when (action) {
+ is ProfileSettingNavigationAction.NavigateToSetProfileImage -> editProfileImageBottomSheet()
+
+ is ProfileSettingNavigationAction.NavigateToHome -> {
+ startActivity(MainActivity.getIntent(this))
+ finish()
+ }
+ }
+ }
+ }
+
+ private fun initImageLaunchers() {
+ cropImageOptions =
+ CropImageOptions(
+ fixAspectRatio = true,
+ aspectRatioX = 1,
+ aspectRatioY = 1,
+ toolbarColor = Color.WHITE,
+ toolbarBackButtonColor = Color.BLACK,
+ toolbarTintColor = Color.BLACK,
+ allowFlipping = false,
+ allowRotation = false,
+ cropMenuCropButtonTitle = getString(R.string.image_cropper_done),
+ imageSourceIncludeCamera = false,
+ )
+
+ imagePickerLauncher =
+ registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ if (uri == null) return@registerForActivityResult
+ val cropOptions = CropImageContractOptions(uri, cropImageOptions)
+ imageCropLauncher.launch(cropOptions)
+ }
+
+ imageCropLauncher =
+ registerForActivityResult(CropImageContract()) { result ->
+ if (result.isSuccessful) {
+ val uri = result.uriContent ?: return@registerForActivityResult
+ handleCroppedImage(uri = uri)
+ }
+ }
+ }
+
+ private fun handleCroppedImage(uri: Uri) {
+ val bitmap = uri.toBitmap(this)
+ viewModel.updateProfileImage(bitmap)
+ val file = saveBitmapToFile(this, bitmap)
+ val partBody = file.toMultipartBody()
+ viewModel.updateProfileFile(partBody)
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private fun initEditText() {
+ binding.etUserName.customOnFocusChangeListener(this)
+ binding.constraintLayoutProfileSetMain.setOnTouchListener { _, _ ->
+ hideKeyboard()
+ binding.etUserName.clearFocus()
+ false
+ }
+ }
+
+ private fun editProfileImageBottomSheet() {
+ val dialog =
+ EditProfileImageBottomSheet(
+ clickGallery = { imagePickerLauncher.launch("image/*") },
+ clickDefaultImage = { viewModel.resetProfileImage() },
+ )
+
+ dialog.show(supportFragmentManager, "TAG")
+ }
+
+ companion object {
+ fun getIntent(context: Context): Intent {
+ return Intent(context, ProfileSettingActivity::class.java)
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingNavigationAction.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingNavigationAction.kt
new file mode 100644
index 000000000..32d6390db
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingNavigationAction.kt
@@ -0,0 +1,7 @@
+package com.woowacourse.friendogly.presentation.ui.profilesetting
+
+sealed interface ProfileSettingNavigationAction {
+ data object NavigateToSetProfileImage : ProfileSettingNavigationAction
+
+ data object NavigateToHome : ProfileSettingNavigationAction
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingUiState.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingUiState.kt
new file mode 100644
index 000000000..dd7a82cc8
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingUiState.kt
@@ -0,0 +1,9 @@
+package com.woowacourse.friendogly.presentation.ui.profilesetting
+
+import android.graphics.Bitmap
+import okhttp3.MultipartBody
+
+data class ProfileSettingUiState(
+ val profileImage: Bitmap? = null,
+ val profilePath: MultipartBody.Part? = null,
+)
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingViewModel.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingViewModel.kt
new file mode 100644
index 000000000..d50d41e4a
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/ProfileSettingViewModel.kt
@@ -0,0 +1,44 @@
+package com.woowacourse.friendogly.presentation.ui.profilesetting
+
+import android.graphics.Bitmap
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.woowacourse.friendogly.presentation.base.BaseViewModel
+import com.woowacourse.friendogly.presentation.base.Event
+import com.woowacourse.friendogly.presentation.base.emit
+import okhttp3.MultipartBody
+
+class ProfileSettingViewModel : BaseViewModel() {
+ private val _uiState: MutableLiveData =
+ MutableLiveData(ProfileSettingUiState())
+ val uiState: LiveData get() = _uiState
+
+ val nickname = MutableLiveData("")
+
+ private val _navigateAction: MutableLiveData> =
+ MutableLiveData(null)
+ val navigateAction: LiveData> get() = _navigateAction
+
+ fun selectProfileImage() {
+ _navigateAction.emit(ProfileSettingNavigationAction.NavigateToSetProfileImage)
+ }
+
+ fun submitProfileSelection() {
+ _navigateAction.emit(ProfileSettingNavigationAction.NavigateToHome)
+ }
+
+ fun updateProfileImage(bitmap: Bitmap) {
+ val state = _uiState.value ?: return
+ _uiState.value = state.copy(profileImage = bitmap)
+ }
+
+ fun resetProfileImage() {
+ val state = _uiState.value ?: return
+ _uiState.value = state.copy(profileImage = null)
+ }
+
+ fun updateProfileFile(file: MultipartBody.Part) {
+ val state = _uiState.value ?: return
+ _uiState.value = state.copy(profilePath = file)
+ }
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/bottom/EditProfileImageBottomSheet.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/bottom/EditProfileImageBottomSheet.kt
new file mode 100644
index 000000000..1fee12dad
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/profilesetting/bottom/EditProfileImageBottomSheet.kt
@@ -0,0 +1,66 @@
+package com.woowacourse.friendogly.presentation.ui.profilesetting.bottom
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.TextView
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.woowacourse.friendogly.R
+
+class EditProfileImageBottomSheet(
+ private val clickGallery: () -> Unit,
+ private val clickDefaultImage: () -> Unit,
+) : BottomSheetDialogFragment() {
+ private lateinit var dlg: BottomSheetDialog
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ dlg = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
+ dlg.setOnShowListener {
+ val bottomSheet = dlg.findViewById(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout
+ bottomSheet.setBackgroundResource(android.R.color.transparent)
+
+ val behavior = BottomSheetBehavior.from(bottomSheet)
+ behavior.isDraggable = true
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ return dlg
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View? {
+ return inflater.inflate(R.layout.bottom_sheet_edit_profile_image, container, false)
+ }
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?,
+ ) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val gallery = view.findViewById(R.id.btn_gallery_select)
+ val defaultImage = view.findViewById(R.id.btn_default_image_set)
+ val close = view.findViewById(R.id.close_btn)
+
+ gallery.setOnClickListener {
+ clickGallery.invoke()
+ dismiss()
+ }
+
+ defaultImage.setOnClickListener {
+ clickDefaultImage.invoke()
+ dismiss()
+ }
+
+ close.setOnClickListener {
+ dismiss()
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/GoogleSignInContract.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/GoogleSignInContract.kt
new file mode 100644
index 000000000..fb89c4218
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/GoogleSignInContract.kt
@@ -0,0 +1,38 @@
+package com.woowacourse.friendogly.presentation.ui.register
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.google.android.gms.tasks.Task
+import com.woowacourse.friendogly.BuildConfig
+
+class GoogleSignInContract : ActivityResultContract?>() {
+ override fun createIntent(
+ context: Context,
+ input: Int,
+ ): Intent {
+ val googleSignInClient =
+ GoogleSignIn.getClient(
+ context,
+ GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+ .requestIdToken(BuildConfig.GOOGLE_CLIENT_ID)
+ .requestServerAuthCode(BuildConfig.GOOGLE_CLIENT_ID)
+ .build(),
+ )
+ return googleSignInClient.signInIntent
+ }
+
+ override fun parseResult(
+ resultCode: Int,
+ intent: Intent?,
+ ): Task? {
+ return when (resultCode) {
+ Activity.RESULT_OK -> GoogleSignIn.getSignedInAccountFromIntent(intent)
+ else -> null
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterActivity.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterActivity.kt
index 0e24792c0..96dc389b7 100644
--- a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterActivity.kt
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterActivity.kt
@@ -1,20 +1,34 @@
package com.woowacourse.friendogly.presentation.ui.register
import androidx.activity.viewModels
+import com.google.android.gms.common.api.ApiException
import com.woowacourse.friendogly.R
import com.woowacourse.friendogly.databinding.ActivityRegisterBinding
import com.woowacourse.friendogly.presentation.base.BaseActivity
import com.woowacourse.friendogly.presentation.base.observeEvent
import com.woowacourse.friendogly.presentation.ui.MainActivity
+import com.woowacourse.friendogly.presentation.ui.profilesetting.ProfileSettingActivity
class RegisterActivity : BaseActivity(R.layout.activity_register) {
private val viewModel: RegisterViewModel by viewModels()
+ private val googleSignInLauncher =
+ registerForActivityResult(GoogleSignInContract()) { task ->
+ val account =
+ task?.getResult(ApiException::class.java) ?: return@registerForActivityResult
+ val idToken = account.idToken ?: return@registerForActivityResult
+ viewModel.handleGoogleLogin(idToken = idToken)
+ }
+
override fun initCreateView() {
- binding.vm = viewModel
+ initDataBinding()
initObserve()
}
+ private fun initDataBinding() {
+ binding.vm = viewModel
+ }
+
private fun initObserve() {
viewModel.navigateAction.observeEvent(this) { action ->
when (action) {
@@ -23,11 +37,16 @@ class RegisterActivity : BaseActivity(R.layout.activity
finish()
}
- is RegisterNavigationAction.NavigateToGoogleLogin -> {
- startActivity(MainActivity.getIntent(this))
- finish()
- }
+ is RegisterNavigationAction.NavigateToGoogleLogin ->
+ googleSignInLauncher.launch(SIGN_IN_REQUEST_CODE)
+
+ is RegisterNavigationAction.NavigateToProfileSetting ->
+ startActivity(ProfileSettingActivity.getIntent(this))
}
}
}
+
+ companion object {
+ const val SIGN_IN_REQUEST_CODE = 1
+ }
}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterNavigationAction.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterNavigationAction.kt
index 4e0f70122..17ccb4113 100644
--- a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterNavigationAction.kt
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterNavigationAction.kt
@@ -4,4 +4,6 @@ sealed interface RegisterNavigationAction {
data object NavigateToKakaoLogin : RegisterNavigationAction
data object NavigateToGoogleLogin : RegisterNavigationAction
+
+ data class NavigateToProfileSetting(val idToken: String) : RegisterNavigationAction
}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterViewModel.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterViewModel.kt
index f92891c88..07beeab00 100644
--- a/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterViewModel.kt
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/ui/register/RegisterViewModel.kt
@@ -18,4 +18,8 @@ class RegisterViewModel : BaseViewModel() {
fun executeGoogleLogin() {
_navigateAction.emit(RegisterNavigationAction.NavigateToGoogleLogin)
}
+
+ fun handleGoogleLogin(idToken: String) {
+ _navigateAction.emit(RegisterNavigationAction.NavigateToProfileSetting(idToken = idToken))
+ }
}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/BitmapUtil.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/BitmapUtil.kt
new file mode 100644
index 000000000..ce9355bb9
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/BitmapUtil.kt
@@ -0,0 +1,49 @@
+package com.woowacourse.friendogly.presentation.utils
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+
+@Throws(IOException::class)
+fun saveBitmapToFile(
+ context: Context,
+ bitmap: Bitmap,
+): File {
+ val directory = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "images")
+ if (!directory.exists()) {
+ directory.mkdirs()
+ }
+ val file = File(directory, "image.jpg")
+ val outputStream = FileOutputStream(file)
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
+ outputStream.flush()
+ outputStream.close()
+ return file
+}
+
+fun File.toMultipartBody(): MultipartBody.Part {
+ val requestFile = this.asRequestBody("image/*".toMediaTypeOrNull())
+ return MultipartBody.Part.createFormData("file", this.name, requestFile)
+}
+
+fun Uri.toBitmap(context: Context): Bitmap =
+ if (Build.VERSION.SDK_INT < 28) {
+ MediaStore.Images.Media.getBitmap(context.contentResolver, this)
+ } else {
+ val source = ImageDecoder.createSource(context.contentResolver, this)
+ ImageDecoder.decodeBitmap(source)
+ }
+
+fun Bitmap.toSoftwareBitmap(): Bitmap {
+ return copy(Bitmap.Config.ARGB_8888, true)
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/EditTextExtension.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/EditTextExtension.kt
new file mode 100644
index 000000000..41c0ced16
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/EditTextExtension.kt
@@ -0,0 +1,20 @@
+package com.woowacourse.friendogly.presentation.utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.View
+import android.widget.EditText
+import com.woowacourse.friendogly.R
+
+@SuppressLint("UseCompatLoadingForDrawables")
+fun EditText.customOnFocusChangeListener(context: Context) {
+ this.onFocusChangeListener =
+ View.OnFocusChangeListener { view, gainFocus ->
+ if (gainFocus) {
+ view.background =
+ context.getDrawable(R.drawable.rect_gray03_line_gray06_16)
+ } else {
+ view.background = context.getDrawable(R.drawable.rect_gray03_fill_16)
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/GlideImageBindingAdapters.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/GlideImageBindingAdapters.kt
new file mode 100644
index 000000000..deaf91dcb
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/GlideImageBindingAdapters.kt
@@ -0,0 +1,33 @@
+package com.woowacourse.friendogly.presentation.utils
+
+import android.graphics.Bitmap
+import android.widget.ImageView
+import androidx.databinding.BindingAdapter
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.woowacourse.friendogly.R
+
+@BindingAdapter("glide1000")
+fun ImageView.bindGlide1000(uri: String?) {
+ if (uri == null) return
+ Glide.with(context)
+ .load(uri)
+ .transform(CenterCrop(), RoundedCorners(1000))
+ .into(this)
+}
+
+@BindingAdapter("glideProfile1000")
+fun ImageView.bindProfile1000(bitmap: Bitmap?) {
+ if (bitmap == null) {
+ this.setImageResource(R.drawable.img_dog)
+ return
+ }
+ val softwareBitmap = bitmap.toSoftwareBitmap()
+
+ Glide.with(context)
+ .asBitmap()
+ .load(softwareBitmap)
+ .transform(CenterCrop(), RoundedCorners(1000))
+ .into(this)
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/KeyboardExtensions.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/KeyboardExtensions.kt
new file mode 100644
index 000000000..b9a412121
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/KeyboardExtensions.kt
@@ -0,0 +1,23 @@
+package com.woowacourse.friendogly.presentation.utils
+
+import android.app.Activity
+import android.content.Context
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+
+fun Activity.hideKeyboard() {
+ if (this.currentFocus != null) {
+ val inputManager: InputMethodManager =
+ this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ inputManager.hideSoftInputFromWindow(
+ this.currentFocus?.windowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS,
+ )
+ }
+}
+
+fun Activity.showKeyboard(view: View) {
+ val inputManager: InputMethodManager =
+ this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
+}
diff --git a/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/ParcelExtension.kt b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/ParcelExtension.kt
new file mode 100644
index 000000000..e1a8d63c8
--- /dev/null
+++ b/android/app/src/main/java/com/woowacourse/friendogly/presentation/utils/ParcelExtension.kt
@@ -0,0 +1,15 @@
+package com.woowacourse.friendogly.presentation.utils
+
+import android.os.Build
+import android.os.Bundle
+
+inline fun Bundle.bundleParcelable(
+ key: String,
+ clazz: Class,
+): T? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ this.getParcelable(key, clazz)
+ } else {
+ this.getParcelable(key)
+ }
+}
diff --git a/android/app/src/main/res/drawable/ic_user_profile_set_button.xml b/android/app/src/main/res/drawable/ic_user_profile_set_button.xml
new file mode 100644
index 000000000..1349c0b7b
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_user_profile_set_button.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/img_woof_bottom_sheet_dog.png b/android/app/src/main/res/drawable/img_woof_bottom_sheet_dog.png
new file mode 100644
index 000000000..f64ca1c08
Binary files /dev/null and b/android/app/src/main/res/drawable/img_woof_bottom_sheet_dog.png differ
diff --git a/android/app/src/main/res/drawable/rect_blue_fill_16.xml b/android/app/src/main/res/drawable/rect_blue_fill_16.xml
new file mode 100644
index 000000000..236b0da1b
--- /dev/null
+++ b/android/app/src/main/res/drawable/rect_blue_fill_16.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/rect_gray03_fill_16.xml b/android/app/src/main/res/drawable/rect_gray03_fill_16.xml
new file mode 100644
index 000000000..62e1dfbf0
--- /dev/null
+++ b/android/app/src/main/res/drawable/rect_gray03_fill_16.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/rect_gray03_line_gray06_16.xml b/android/app/src/main/res/drawable/rect_gray03_line_gray06_16.xml
new file mode 100644
index 000000000..934693ee6
--- /dev/null
+++ b/android/app/src/main/res/drawable/rect_gray03_line_gray06_16.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/rect_orange100_fill_12.xml b/android/app/src/main/res/drawable/rect_orange100_fill_12.xml
new file mode 100644
index 000000000..a813232bc
--- /dev/null
+++ b/android/app/src/main/res/drawable/rect_orange100_fill_12.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/rect_orange700_fill_12.xml b/android/app/src/main/res/drawable/rect_orange700_fill_12.xml
new file mode 100644
index 000000000..7794a7f9b
--- /dev/null
+++ b/android/app/src/main/res/drawable/rect_orange700_fill_12.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/rect_white_fill_10.xml b/android/app/src/main/res/drawable/rect_white_fill_10.xml
new file mode 100644
index 000000000..8d7af8f4a
--- /dev/null
+++ b/android/app/src/main/res/drawable/rect_white_fill_10.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/activity_profile_setting.xml b/android/app/src/main/res/layout/activity_profile_setting.xml
new file mode 100644
index 000000000..ec589696b
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_profile_setting.xml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/activity_register.xml b/android/app/src/main/res/layout/activity_register.xml
index e34897557..fd457140a 100644
--- a/android/app/src/main/res/layout/activity_register.xml
+++ b/android/app/src/main/res/layout/activity_register.xml
@@ -40,6 +40,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/bottom_sheet_woof.xml b/android/app/src/main/res/layout/bottom_sheet_woof.xml
new file mode 100644
index 000000000..0d14ea780
--- /dev/null
+++ b/android/app/src/main/res/layout/bottom_sheet_woof.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/fragment_chat_list.xml b/android/app/src/main/res/layout/fragment_chat_list.xml
new file mode 100644
index 000000000..b59e39b1c
--- /dev/null
+++ b/android/app/src/main/res/layout/fragment_chat_list.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/item_chat.xml b/android/app/src/main/res/layout/item_chat.xml
new file mode 100644
index 000000000..c91d251c5
--- /dev/null
+++ b/android/app/src/main/res/layout/item_chat.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/navigation/navigation_main.xml b/android/app/src/main/res/navigation/navigation_main.xml
index 0aff73b7d..639c5fe8f 100644
--- a/android/app/src/main/res/navigation/navigation_main.xml
+++ b/android/app/src/main/res/navigation/navigation_main.xml
@@ -15,6 +15,16 @@
app:popUpTo="@id/navigation_graph"
app:popUpToInclusive="true" />
+
+
+
+
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 6fecc12fc..6cbb57935 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -15,4 +15,6 @@
#EEEEEE
#F5F5F5
#FAFAFA
+
+ #8AABFF
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index b02806387..f0598c325 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -4,6 +4,13 @@
간편하게 로그인 할래요
+
+ 닉네임
+ 닉네임을 입력해주세요
+ 프로필 사진
+ 프로필 만들기
+ 선택 완료!
+
뒤로가기 버튼을\n한번 더 누르면 종료됩니다.
@@ -13,4 +20,12 @@
채팅
나의 댕댕이
+
+ 앨범에서 사진 선택
+ 기본 이미지로 변경
+ 닫기
+
+
+ 확인
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 31b90d886..6e00d3f19 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,10 +1,30 @@
+
+
+
+
+
+
+
+
+
+
@@ -13,11 +33,13 @@
@@ -25,23 +47,27 @@
@@ -49,17 +75,20 @@
@@ -68,47 +97,55 @@
@@ -117,6 +154,7 @@
@@ -124,16 +162,19 @@
@@ -141,23 +182,27 @@
@@ -165,25 +210,53 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
index 7e9adff00..f4d2317c6 100644
--- a/android/app/src/main/res/values/themes.xml
+++ b/android/app/src/main/res/values/themes.xml
@@ -6,4 +6,11 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index d2c0f50fa..773311cde 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -15,6 +15,7 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.ktlint) apply false
alias(libs.plugins.navigation.safeargs) apply false
+ kotlin("plugin.serialization") version "1.9.0" apply false
}
allprojects {