Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/#40 source data store #16

Merged
merged 10 commits into from
Sep 28, 2023
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ dependencies {
implementation "com.kakao.sdk:v2-share:2.15.0" // 메시지(카카오톡 공유)
//EncryptedSharedPreference
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha06"
// dataStore
implementation "androidx.datastore:datastore-preferences:1.0.0"


}
// hilt dependency와 함께 추가
kapt {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.puzzling.puzzlingaos.data.datasource.local

import android.os.Build
import androidx.annotation.RequiresApi
import androidx.datastore.core.Serializer
import com.puzzling.puzzlingaos.data.entity.Token
import com.puzzling.puzzlingaos.data.service.CryptoService
import kotlinx.serialization.json.Json
import org.apache.commons.lang3.SerializationException
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject

@RequiresApi(Build.VERSION_CODES.M)
class TokenDataSource @Inject constructor(private val cryptoService: CryptoService) : Serializer<Token> {
override val defaultValue: Token
get() = Token()

override suspend fun readFrom(input: InputStream): Token {
val decryptedBytes = cryptoService.decrypt(input)
return try {
Json.decodeFromString(
deserializer = Token.serializer(),
string = decryptedBytes.decodeToString(),
)
} catch (e: SerializationException) {
e.printStackTrace()
defaultValue
}
}

override suspend fun writeTo(t: Token, output: OutputStream) {
cryptoService.encrypt(
bytes = Json.encodeToString(
serializer = Token.serializer(),
value = t,
).encodeToByteArray(),
outputStream = output,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.puzzling.puzzlingaos.data.entity

import kotlinx.serialization.Serializable

@Serializable
data class Token(
val accessToken: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.puzzling.puzzlingaos.data.repository

import androidx.datastore.core.DataStore
import com.puzzling.puzzlingaos.data.entity.Token
import com.puzzling.puzzlingaos.domain.repository.TokenRepository
import kotlinx.coroutines.flow.first
import javax.inject.Inject

class TokenRepositoryImpl @Inject constructor(private val dataStore: DataStore<Token>) :
TokenRepository {
override suspend fun setToken(token: String) {
dataStore.updateData { Token(token) }
}

override suspend fun getToken(): Token = dataStore.data.first()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.puzzling.puzzlingaos.data.service

import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import java.io.InputStream
import java.io.OutputStream
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.inject.Inject

@RequiresApi(Build.VERSION_CODES.M)
class CryptoService @Inject constructor() {

private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}

private val encryptCipher = Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.ENCRYPT_MODE, getKey())
}

private fun getDecryptCipher(iv: ByteArray): Cipher {
return Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))
}
}

private fun getKey(): SecretKey {
val existingKey = keyStore.getEntry("secret", null) as? KeyStore.SecretKeyEntry
return existingKey?.secretKey ?: createKey()
}

private fun createKey(): SecretKey {
return KeyGenerator.getInstance(ALGORITHM).apply {
init(
KeyGenParameterSpec.Builder(
"secret",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
).setBlockModes(BLOCK_MODE).setEncryptionPaddings(
PADDING,
).setUserAuthenticationRequired(false) // 지문인식 같은거 false
.setRandomizedEncryptionRequired(true).build(),
)
}.generateKey()
}

fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
val encryptedBytes = encryptCipher.doFinal(bytes)
outputStream.use {
it.write(encryptCipher.iv.size)
it.write(encryptCipher.iv)
it.write(encryptedBytes.size)
it.write(encryptedBytes)
}

return encryptedBytes
}

fun decrypt(inputStream: InputStream): ByteArray {
return inputStream.use {
val ivSize = it.read()
val iv = ByteArray(ivSize)
it.read(iv)

val encryptedBytesSize = it.read()
val encryptedBytes = ByteArray(encryptedBytesSize)
it.read(encryptedBytes)

getDecryptCipher(iv).doFinal(encryptedBytes)
}
}

companion object {
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package com.puzzling.puzzlingaos.data.service
import android.content.Context
import com.kakao.sdk.auth.model.OAuthToken
import com.kakao.sdk.user.UserApiClient
import dagger.hilt.android.qualifiers.ActivityContext
import javax.inject.Inject

class KakaoLoginService(private val context: Context) {
class KakaoAuthService @Inject constructor(@ActivityContext private val context: Context) {
fun startKakaoLogin(kakaoLoginCallBack: (OAuthToken?, Throwable?) -> Unit) {
val kakaoLoginState =
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
Expand All @@ -20,6 +22,7 @@ class KakaoLoginService(private val context: Context) {
callback = kakaoLoginCallBack,
)
}

KAKAO_ACCOUNT_LOGIN -> {
UserApiClient.instance.loginWithKakaoAccount(
context,
Expand Down
35 changes: 35 additions & 0 deletions app/src/main/java/com/puzzling/puzzlingaos/di/DataStoreModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.puzzling.puzzlingaos.di

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.puzzling.puzzlingaos.data.datasource.local.TokenDataSource
import com.puzzling.puzzlingaos.data.entity.Token
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
private const val USER_PREFERENCES_NAME = "user_preferences"
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"

@Provides
@Singleton
fun providePreferencesDataStore(
@ApplicationContext appContext: Context,
tokenDataSource: TokenDataSource,
): DataStore<Token> {
return DataStoreFactory.create(
serializer = tokenDataSource,
produceFile = {
appContext.dataStoreFile(DATA_STORE_FILE_NAME)
},
)
}
}
16 changes: 6 additions & 10 deletions app/src/main/java/com/puzzling/puzzlingaos/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
package com.puzzling.puzzlingaos.di

import com.puzzling.puzzlingaos.data.repository.MyBoardRepositoryImpl
import com.puzzling.puzzlingaos.data.repository.MyPageRepositoryImpl
import com.puzzling.puzzlingaos.data.repository.ProjectRepositoryImpl
import com.puzzling.puzzlingaos.data.repository.TeamDashBoardRepositoryImpl
import com.puzzling.puzzlingaos.data.repository.WriteReviewRepositoryImpl
import com.puzzling.puzzlingaos.domain.repository.MyBoardRepository
import com.puzzling.puzzlingaos.domain.repository.MyPageRepository
import com.puzzling.puzzlingaos.domain.repository.ProjectRepository
import com.puzzling.puzzlingaos.domain.repository.TeamDashBoardRepository
import com.puzzling.puzzlingaos.domain.repository.WriteReviewRepository
import com.puzzling.puzzlingaos.data.repository.*
import com.puzzling.puzzlingaos.domain.repository.*
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -38,4 +30,8 @@ abstract class RepositoryModule {
@Singleton
@Binds
abstract fun providesWriteReviewRepository(repoImpl: WriteReviewRepositoryImpl): WriteReviewRepository

@Singleton
@Binds
abstract fun providesTokenRepository(repoImpl: TokenRepositoryImpl): TokenRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.puzzling.puzzlingaos.domain.repository

import com.puzzling.puzzlingaos.data.entity.Token

interface TokenRepository {
suspend fun setToken(token: String)

suspend fun getToken(): Token
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.puzzling.puzzlingaos.domain.usecase.onboarding

import com.puzzling.puzzlingaos.domain.repository.TokenRepository
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class GetTokenUseCase @Inject constructor(
private val tokenRepository: TokenRepository,
) {

suspend operator fun invoke() = tokenRepository.getToken()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.puzzling.puzzlingaos.domain.usecase.onboarding

import com.puzzling.puzzlingaos.domain.repository.TokenRepository
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PostTokenUseCase @Inject constructor(private val tokenRepository: TokenRepository) {
suspend operator fun invoke(token: String) = tokenRepository.setToken(token)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ package com.puzzling.puzzlingaos.presentation.onboarding

import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.puzzling.puzzlingaos.R
import com.puzzling.puzzlingaos.base.BaseActivity
import com.puzzling.puzzlingaos.data.service.KakaoAuthService
import com.puzzling.puzzlingaos.databinding.ActivityLoginBinding
import com.puzzling.puzzlingaos.util.ViewModelFactory
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class LoginActivity : BaseActivity<ActivityLoginBinding>(R.layout.activity_login) {

private val viewModel: LoginViewModel by viewModels { ViewModelFactory(this) }
@Inject
lateinit var kakakoAuthService: KakaoAuthService

private val viewModel: LoginViewModel by viewModels()


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -24,13 +32,13 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(R.layout.activity_login

private fun startKakaoLogin() {
binding.ibLoginKakao.setOnClickListener {
viewModel.kakaoLogin()
kakakoAuthService.startKakaoLogin(viewModel.kakaoLoginCallback)
}
}

private fun isKakaoLoginSuccess() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isKakaoLogin.collect { isLoginSuccess ->
if (isLoginSuccess) {
val intent =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
package com.puzzling.puzzlingaos.presentation.onboarding

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kakao.sdk.auth.model.OAuthToken
import com.puzzling.puzzlingaos.data.datasource.local.LocalDataSource
import com.puzzling.puzzlingaos.data.service.KakaoLoginService
import com.puzzling.puzzlingaos.domain.usecase.onboarding.GetTokenUseCase
import com.puzzling.puzzlingaos.domain.usecase.onboarding.PostTokenUseCase
import com.puzzling.puzzlingaos.util.KakaoLoginCallback
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

class LoginViewModel(private val kakaoLoginService: KakaoLoginService) : ViewModel() {
@HiltViewModel
class LoginViewModel @Inject constructor(
private val postTokenUseCase: PostTokenUseCase,
private val getTokenUseCase: GetTokenUseCase,
) :
ViewModel() {
private val _isKakaoLogin = MutableStateFlow(false)
val isKakaoLogin = _isKakaoLogin.asStateFlow()

val kakaoLoginCallback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
KakaoLoginCallback {
_isKakaoLogin.value = true
Timber.d("토큰!!!! $token")
Log.d("LoginViewModel", "토큰!! $token")
LocalDataSource.setAccessToken("$token")
viewModelScope.launch {
postTokenUseCase.invoke(it)
Log.d("LoginViewModel", "토큰!! usecase ${getTokenUseCase.invoke()}")
}
}.handleResult(token, error)
}

fun kakaoLogin() = viewModelScope.launch {
kakaoLoginService.startKakaoLogin(kakaoLoginCallback)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ package com.puzzling.puzzlingaos.util
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.puzzling.puzzlingaos.data.service.KakaoLoginService
import com.puzzling.puzzlingaos.presentation.invitationCode.InvitationCodeViewModel
import com.puzzling.puzzlingaos.presentation.onboarding.LoginViewModel

class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
Expand All @@ -14,10 +11,10 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
// InvitationCodeViewModel(context) as T
// }

modelClass.isAssignableFrom(LoginViewModel::class.java) -> {
val repository = KakaoLoginService(context)
LoginViewModel(repository) as T
}
// modelClass.isAssignableFrom(LoginViewModel::class.java) -> {
// val repository = KakaoAuthService(context)
// LoginViewModel(repository) as T
// }

else -> {
throw java.lang.IllegalArgumentException("Unknown ViewModel")
Expand Down
Loading