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 @@ + + + + + + + + + + + + + + + + +