diff --git a/.gitignore b/.gitignore index 98f4581a5..6254be1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,7 @@ release/ !/gradle/wrapper/gradle-wrapper.jar -# End of https://www.toptal.com/developers/gitignore/api/kotlin,androidstudio \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/kotlin,androidstudio +/.idea/sonarlint/* +/.idea/dbnavigator.xml +/.idea/migrations.xml diff --git a/app/build.gradle b/app/build.gradle index e22dc8981..0230dbf1f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId "org.sopt.havit" minSdk 23 targetSdk 33 - versionCode 110 - versionName "1.0.10" + versionCode 111 + versionName "1.0.11" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "HAVIT_BASE_URL_DEV", properties["HAVIT_BASE_URL_DEV"]) buildConfigField("String", "HAVIT_BASE_URL_PROD", properties["HAVIT_BASE_URL_PROD"]) @@ -137,13 +137,14 @@ dependencies { implementation 'com.google.firebase:firebase-messaging-ktx' implementation 'com.google.firebase:firebase-crashlytics-ktx' implementation 'com.google.firebase:firebase-analytics-ktx' + implementation 'com.google.firebase:firebase-config-ktx' + // Jsoup implementation 'org.jsoup:jsoup:1.13.1' // Splash Screen implementation 'androidx.core:core-splashscreen:1.0.0-rc01' - // gson implementation "com.google.code.gson:gson:2.8.8" //to use SerializedName diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 692aaca8a..621eb38da 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,10 @@ - + @@ -24,7 +25,13 @@ android:roundIcon="@mipmap/ic_launcher_havit_round" android:supportsRtl="true" android:theme="@style/Theme.Havit" - android:usesCleartextTraffic="true"> + android:usesCleartextTraffic="true" + tools:ignore="LockedOrientationActivity"> + + + + android:screenOrientation="portrait" + android:windowSoftInputMode="stateVisible" /> - + \ No newline at end of file diff --git a/app/src/main/java/org/sopt/havit/data/repository/SystemMaintenanceRepositoryImpl.kt b/app/src/main/java/org/sopt/havit/data/repository/SystemMaintenanceRepositoryImpl.kt new file mode 100644 index 000000000..b5755eae2 --- /dev/null +++ b/app/src/main/java/org/sopt/havit/data/repository/SystemMaintenanceRepositoryImpl.kt @@ -0,0 +1,31 @@ +package org.sopt.havit.data.repository + +import org.sopt.havit.data.source.remote.RemoteConfigDataSource +import org.sopt.havit.domain.repository.SystemMaintenanceRepository +import javax.inject.Inject + +class SystemMaintenanceRepositoryImpl @Inject constructor( + private val systemMaintenanceRemoteDataSource: RemoteConfigDataSource, +) : SystemMaintenanceRepository { + override suspend fun isSystemMaintenance(): Boolean { + val isSystemUnderMaintenance = systemMaintenanceRemoteDataSource.fetchRemoteConfig( + IS_SYSTEM_UNDER_MAINTENANCE, + Boolean::class.java + ) as? Boolean + return isSystemUnderMaintenance ?: false + } + + override suspend fun getSystemMaintenanceMessage(): String { + val message = systemMaintenanceRemoteDataSource.fetchRemoteConfig( + SYSTEM_MAINTENANCE_MESSAGE, + String::class.java + ).toString() + return message.ifEmpty { DEFAULT_MESSAGE } + } + + companion object { + private const val IS_SYSTEM_UNDER_MAINTENANCE = "isSystemUnderMaintenance" + private const val SYSTEM_MAINTENANCE_MESSAGE = "systemMaintenanceMessage" + private const val DEFAULT_MESSAGE = "현재 시스템 점검중입니다.\\n불편을 끼쳐드려 죄송합니다." + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/havit/data/source/remote/RemoteConfigDataSource.kt b/app/src/main/java/org/sopt/havit/data/source/remote/RemoteConfigDataSource.kt new file mode 100644 index 000000000..4424d1135 --- /dev/null +++ b/app/src/main/java/org/sopt/havit/data/source/remote/RemoteConfigDataSource.kt @@ -0,0 +1,7 @@ +package org.sopt.havit.data.source.remote + +import java.lang.reflect.Type + +interface RemoteConfigDataSource { + suspend fun fetchRemoteConfig(configKey: String, valueType: Type): Any +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/havit/data/source/remote/RemoteConfigDataSourceImpl.kt b/app/src/main/java/org/sopt/havit/data/source/remote/RemoteConfigDataSourceImpl.kt new file mode 100644 index 000000000..05149fac1 --- /dev/null +++ b/app/src/main/java/org/sopt/havit/data/source/remote/RemoteConfigDataSourceImpl.kt @@ -0,0 +1,34 @@ +package org.sopt.havit.data.source.remote + +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfigSettings +import kotlinx.coroutines.suspendCancellableCoroutine +import java.lang.reflect.Type +import javax.inject.Inject + +class RemoteConfigDataSourceImpl @Inject constructor() : RemoteConfigDataSource { + + private val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig.apply { + setConfigSettingsAsync(remoteConfigSettings { minimumFetchIntervalInSeconds = 60 }) + } + + override suspend fun fetchRemoteConfig(configKey: String, valueType: Type): Any { + return suspendCancellableCoroutine { continuation -> + remoteConfig.fetchAndActivate().addOnCompleteListener { task -> + if (task.isSuccessful) { + val remoteConfigValue = when (valueType) { + String::class.java -> remoteConfig.getString(configKey) + Boolean::class.java -> remoteConfig.getBoolean(configKey) + Long::class.java -> remoteConfig.getLong(configKey) + else -> throw IllegalArgumentException("Not supported type. Please check valueType") + } + continuation.resumeWith(Result.success(remoteConfigValue)) + } else continuation.resumeWith( + Result.failure(task.exception ?: Exception("fetchRemoteConfig failed")) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/havit/di/DataSourceModule.kt b/app/src/main/java/org/sopt/havit/di/DataSourceModule.kt index 553c7478a..4131481c7 100644 --- a/app/src/main/java/org/sopt/havit/di/DataSourceModule.kt +++ b/app/src/main/java/org/sopt/havit/di/DataSourceModule.kt @@ -10,6 +10,8 @@ import org.sopt.havit.data.source.local.AuthLocalDataSource import org.sopt.havit.data.source.local.AuthLocalDataSourceImpl import org.sopt.havit.data.source.remote.AuthRemoteDataSource import org.sopt.havit.data.source.remote.AuthRemoteDataSourceImpl +import org.sopt.havit.data.source.remote.RemoteConfigDataSource +import org.sopt.havit.data.source.remote.RemoteConfigDataSourceImpl import org.sopt.havit.data.source.remote.SearchRemoteDataSource import org.sopt.havit.data.source.remote.SearchRemoteDataSourceImpl import org.sopt.havit.data.source.remote.category.CategoryRemoteDataSource @@ -46,4 +48,9 @@ object DataSourceModule { @Singleton fun provideCategoryRemoteDataSource(api: HavitApi): CategoryRemoteDataSource = CategoryRemoteDataSourceImpl(api) + + @Provides + @Singleton + fun provideRemoteConfigDataSource(): RemoteConfigDataSource = RemoteConfigDataSourceImpl() + } diff --git a/app/src/main/java/org/sopt/havit/di/RepositoryModule.kt b/app/src/main/java/org/sopt/havit/di/RepositoryModule.kt index ac1b1284d..a2fa49790 100644 --- a/app/src/main/java/org/sopt/havit/di/RepositoryModule.kt +++ b/app/src/main/java/org/sopt/havit/di/RepositoryModule.kt @@ -9,6 +9,7 @@ import org.sopt.havit.data.mapper.ContentsMapper import org.sopt.havit.data.repository.* import org.sopt.havit.data.source.local.AuthLocalDataSourceImpl import org.sopt.havit.data.source.remote.AuthRemoteDataSourceImpl +import org.sopt.havit.data.source.remote.RemoteConfigDataSourceImpl import org.sopt.havit.data.source.remote.SearchRemoteDataSourceImpl import org.sopt.havit.data.source.remote.category.CategoryRemoteDataSourceImpl import org.sopt.havit.data.source.remote.contents.ContentsRemoteDataSourceImpl @@ -22,34 +23,41 @@ object RepositoryModule { @Singleton fun provideSearchRepository( searchRemoteDataSourceImpl: SearchRemoteDataSourceImpl, - contentsMapper: ContentsMapper + contentsMapper: ContentsMapper, ): SearchRepository = SearchRepositoryImpl(searchRemoteDataSourceImpl, contentsMapper) @Provides @Singleton fun provideMyPageRepository( - havitApi: HavitApi + havitApi: HavitApi, ): MyPageRepository = MyPageRepositoryImpl(havitApi) @Provides @Singleton fun provideContentsRepository( contentsRemoteDataSourceImpl: ContentsRemoteDataSourceImpl, - havitApi: HavitApi + havitApi: HavitApi, ): ContentsRepository = ContentsRepositoryImpl(contentsRemoteDataSourceImpl, havitApi) @Provides @Singleton fun provideCategoryRepository( - categoryRemoteDataSourceImpl: CategoryRemoteDataSourceImpl + categoryRemoteDataSourceImpl: CategoryRemoteDataSourceImpl, ): CategoryRepository = CategoryRepositoryImpl(categoryRemoteDataSourceImpl) @Provides @Singleton fun provideAuthRepository( authRemoteDataSourceImpl: AuthRemoteDataSourceImpl, - authLocalDataSourceImpl: AuthLocalDataSourceImpl + authLocalDataSourceImpl: AuthLocalDataSourceImpl, ): AuthRepository = AuthRepositoryImpl(authRemoteDataSourceImpl, authLocalDataSourceImpl) + + + @Provides + @Singleton + fun provideSystemMaintenanceRepository( + systemMaintenanceDataSource: RemoteConfigDataSourceImpl, + ): SystemMaintenanceRepository = SystemMaintenanceRepositoryImpl(systemMaintenanceDataSource) } diff --git a/app/src/main/java/org/sopt/havit/domain/repository/SystemMaintenanceRepository.kt b/app/src/main/java/org/sopt/havit/domain/repository/SystemMaintenanceRepository.kt new file mode 100644 index 000000000..7365544d4 --- /dev/null +++ b/app/src/main/java/org/sopt/havit/domain/repository/SystemMaintenanceRepository.kt @@ -0,0 +1,8 @@ +package org.sopt.havit.domain.repository + +interface SystemMaintenanceRepository { + + suspend fun isSystemMaintenance(): Boolean + + suspend fun getSystemMaintenanceMessage(): String +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/havit/ui/base/BaseBindingActivity.kt b/app/src/main/java/org/sopt/havit/ui/base/BaseBindingActivity.kt index 64fae682d..ff289213b 100644 --- a/app/src/main/java/org/sopt/havit/ui/base/BaseBindingActivity.kt +++ b/app/src/main/java/org/sopt/havit/ui/base/BaseBindingActivity.kt @@ -1,13 +1,18 @@ package org.sopt.havit.ui.base +import android.content.Intent import android.os.Bundle import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding +import androidx.lifecycle.Observer +import org.sopt.havit.ui.system_maintenance.SystemMaintenanceActivity -abstract class BaseBindingActivity(@LayoutRes private val layoutRes: Int) : - AppCompatActivity() { + +abstract class BaseBindingActivity( + @LayoutRes private val layoutRes: Int, +) : AppCompatActivity() { lateinit var binding: T override fun onCreate(savedInstanceState: Bundle?) { @@ -16,5 +21,13 @@ abstract class BaseBindingActivity(@LayoutRes private val l binding.lifecycleOwner = this } + val systemMaintenanceObserver = Observer { isSystemMaintenance -> + if (isSystemMaintenance) startSystemMaintenanceActivity() + } + + private fun startSystemMaintenanceActivity() { + startActivity(Intent(this, SystemMaintenanceActivity::class.java)) + finish() + } } diff --git a/app/src/main/java/org/sopt/havit/ui/base/BaseViewModel.kt b/app/src/main/java/org/sopt/havit/ui/base/BaseViewModel.kt new file mode 100644 index 000000000..4015fc714 --- /dev/null +++ b/app/src/main/java/org/sopt/havit/ui/base/BaseViewModel.kt @@ -0,0 +1,27 @@ +package org.sopt.havit.ui.base + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.havit.domain.repository.SystemMaintenanceRepository +import javax.inject.Inject + +@HiltViewModel +open class BaseViewModel @Inject constructor( + private val systemMaintenanceRepository: SystemMaintenanceRepository, +) : ViewModel() { + + + private val _isSystemMaintenance: MutableLiveData = MutableLiveData() + val isSystemMaintenance: LiveData = _isSystemMaintenance + + + fun fetchIsSystemMaintenance() { + viewModelScope.launch { + _isSystemMaintenance.postValue(systemMaintenanceRepository.isSystemMaintenance()) + } + } +} diff --git a/app/src/main/java/org/sopt/havit/ui/share/ShareActivity.kt b/app/src/main/java/org/sopt/havit/ui/share/ShareActivity.kt index 2b2ca9571..71cf16476 100644 --- a/app/src/main/java/org/sopt/havit/ui/share/ShareActivity.kt +++ b/app/src/main/java/org/sopt/havit/ui/share/ShareActivity.kt @@ -1,5 +1,6 @@ package org.sopt.havit.ui.share +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.content.pm.ActivityInfo @@ -15,81 +16,101 @@ import org.sopt.havit.databinding.ActivityShareBinding import org.sopt.havit.ui.base.BaseBindingActivity import org.sopt.havit.ui.sign.SignInViewModel.Companion.SPLASH_FROM_SHARE import org.sopt.havit.ui.sign.SplashWithSignActivity +import org.sopt.havit.util.INVALID_URL_TYPE +import org.sopt.havit.util.ToastUtil import java.io.Serializable @AndroidEntryPoint class ShareActivity : BaseBindingActivity(R.layout.activity_share) { - private val viewModel: ShareViewModel by viewModels() - private lateinit var splashWithSignActivityLauncher: ActivityResultLauncher + private val shareViewModel: ShareViewModel by viewModels() + private lateinit var splashWithSignLauncher: ActivityResultLauncher override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityShareBinding.inflate(layoutInflater) - setContentView(binding.root) + + shareViewModel.fetchIsSystemMaintenance() + observeSystemUnderMaintenance() + setScreenOrientation() + initializeActivityResultLauncher() + handleShareFlow() + } + + @SuppressLint("SourceLockedOrientationActivity") + private fun setScreenOrientation() { requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - initActivityLauncher() - makeSignIn() - setUrlOnViewModel() - viewModel.setCrawlingContents() } - private fun initActivityLauncher() { - splashWithSignActivityLauncher = + private fun initializeActivityResultLauncher() { + splashWithSignLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - onSlashWithSignActivityFinish(it) + handleSplashActivityResult(it) } } - private fun onSlashWithSignActivityFinish(result: ActivityResult) { + private fun handleSplashActivityResult(result: ActivityResult) { when (result.resultCode) { - Activity.RESULT_OK -> showBottomSheetShareFragment() + Activity.RESULT_OK -> showShareBottomSheet() else -> finish() } } - private fun isEnterWithShareProcess(intent: Intent?): Boolean { - // 공유하기 버튼으로 진입하면 return true - // MainActivity 의 + 버튼으로 진입하면 return false - return (intent?.action == Intent.ACTION_SEND) && (intent.type == "text/plain") + private fun handleShareFlow() { + initiateSignIn() + extractAndSetUrl() + shareViewModel.setCrawlingContents() } - private fun makeSignIn() { - viewModel.makeSignIn( - internetError = { showBottomSheetNetworkErrorFragment() }, - onUnAuthorized = { moveToSplashWithSignActivity() }, - onAuthorized = { showBottomSheetShareFragment() } + private fun initiateSignIn() { + shareViewModel.makeSignIn( + internetError = { showNetworkErrorBottomSheet() }, + onUnAuthorized = { moveToSplashWithSign() }, + onAuthorized = { showShareBottomSheet() } ) } - private fun moveToSplashWithSignActivity() { + private fun moveToSplashWithSign() { val intent = Intent(this, SplashWithSignActivity::class.java).apply { putExtra(WHERE_SPLASH_COME_FROM, SPLASH_FROM_SHARE) } - splashWithSignActivityLauncher.launch(intent) + splashWithSignLauncher.launch(intent) } - private fun showBottomSheetNetworkErrorFragment() { + private fun showNetworkErrorBottomSheet() { val bottomSheet = BottomSheetNetworkErrorFragment().apply { arguments = Bundle().apply { - putSerializable(ON_NETWORK_ERROR_DISMISS, { makeSignIn() } as Serializable) + putSerializable(ON_NETWORK_ERROR_DISMISS, { initiateSignIn() } as Serializable) } } bottomSheet.show(supportFragmentManager, bottomSheet.tag) } - private fun showBottomSheetShareFragment() { + private fun showShareBottomSheet() { val bottomSheet = BottomSheetShareFragment() bottomSheet.show(supportFragmentManager, bottomSheet.tag) } - private fun setUrlOnViewModel() { - val intent = this.intent - val url = - if (isEnterWithShareProcess(intent)) // 공유하기 버튼으로 진입시 - intent?.getStringExtra(Intent.EXTRA_TEXT).toString() - else intent?.getStringExtra("url").toString() // MainActivity + 로 진입시 - viewModel.setUrl(url) + private fun extractAndSetUrl() { + val url = getUrlFromExtra() + try { + checkUrlNotNull(url) + shareViewModel.setUrl(url.toString()) + } catch (e: IllegalStateException) { + onUrlInvalid() + } + } + + private fun getUrlFromExtra(): String? { + return intent?.getStringExtra(Intent.EXTRA_TEXT) ?: intent?.getStringExtra("url") + } + + private fun checkUrlNotNull(url: String?) { + requireNotNull(url) { throw IllegalStateException() } + } + + private fun onUrlInvalid() { + ToastUtil(this).makeToast(INVALID_URL_TYPE) + finish() } override fun setRequestedOrientation(requestedOrientation: Int) { @@ -97,7 +118,11 @@ class ShareActivity : BaseBindingActivity(R.layout.activit super.setRequestedOrientation(requestedOrientation) } } - + + private fun observeSystemUnderMaintenance() { + shareViewModel.isSystemMaintenance.observe(this, systemMaintenanceObserver) + } + companion object { const val WHERE_SPLASH_COME_FROM = "WHERE_SPLASH_COME_FROM" const val ON_NETWORK_ERROR_DISMISS = "ON_NETWORK_ERROR_DISMISS" diff --git a/app/src/main/java/org/sopt/havit/ui/share/ShareViewModel.kt b/app/src/main/java/org/sopt/havit/ui/share/ShareViewModel.kt index 7267840f6..bd3f37fa1 100644 --- a/app/src/main/java/org/sopt/havit/ui/share/ShareViewModel.kt +++ b/app/src/main/java/org/sopt/havit/ui/share/ShareViewModel.kt @@ -2,14 +2,12 @@ package org.sopt.havit.ui.share import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jsoup.Jsoup import org.jsoup.nodes.Document -import org.sopt.havit.BuildConfig import org.sopt.havit.data.api.HavitApi import org.sopt.havit.data.mapper.CategoryMapper import org.sopt.havit.data.remote.ContentsSummeryData @@ -17,6 +15,8 @@ import org.sopt.havit.data.remote.CreateContentsRequest import org.sopt.havit.domain.entity.CategoryWithSelected import org.sopt.havit.domain.model.NetworkStatus import org.sopt.havit.domain.repository.AuthRepository +import org.sopt.havit.domain.repository.SystemMaintenanceRepository +import org.sopt.havit.ui.base.BaseViewModel import org.sopt.havit.ui.share.notification.AfterTime import org.sopt.havit.util.CalenderUtil import org.sopt.havit.util.HavitAuthUtil @@ -29,8 +29,9 @@ import javax.inject.Inject class ShareViewModel @Inject constructor( private val authRepository: AuthRepository, private val categoryMapper: CategoryMapper, - private val havitApi: HavitApi -) : ViewModel() { + private val havitApi: HavitApi, + systemMaintenanceRepository: SystemMaintenanceRepository, +) : BaseViewModel(systemMaintenanceRepository) { /** token */ fun getAccessToken() = authRepository.getAccessToken() @@ -38,7 +39,7 @@ class ShareViewModel @Inject constructor( fun makeSignIn( internetError: () -> Unit, onUnAuthorized: () -> Unit, - onAuthorized: () -> Unit + onAuthorized: () -> Unit, ) { HavitAuthUtil.isLoginNow({ isInternetNotConnected -> if (isInternetNotConnected) internetError() @@ -113,7 +114,7 @@ class ShareViewModel @Inject constructor( _url.value = extractUrl(url) } - private fun extractUrl(content: String?): String { + private fun extractUrl(content: String): String { val urlPattern = Pattern.compile( "(?:^|\\W)((ht|f)tp(s?)://|www\\.)" + "(([\\w\\-]+\\.)+?([\\w\\-.~]+/?)*" @@ -124,9 +125,8 @@ class ShareViewModel @Inject constructor( while (matcher.find()) { val matchStart = matcher.start(1) val matchEnd = matcher.end() - return content?.substring(matchStart, matchEnd) ?: "" + return content.substring(matchStart, matchEnd) } - if (BuildConfig.IS_DEV) return "https://www.havit.app/" throw IllegalStateException() } @@ -144,7 +144,7 @@ class ShareViewModel @Inject constructor( get() = _tempIndex private var _finalIndex = MutableLiveData() - val finalIndex: LiveData + private val finalIndex: LiveData get() = _finalIndex fun syncTempDataWithFinalData() { @@ -196,15 +196,10 @@ class ShareViewModel @Inject constructor( } } - private fun setDefaultIfTitleDataNotExist() { - if (ogData.value?.ogTitle.isNullOrBlank()) - _ogData.value?.ogTitle = NO_TITLE_CONTENTS - } - private suspend fun getOgData() { viewModelScope.launch(Dispatchers.IO) { kotlin.runCatching { - Jsoup.connect(url.value).get() + Jsoup.connect(url.value).timeout(5000).get() }.onSuccess { val contentsSummeryData = getDataByOgTags(it) _ogData.postValue(contentsSummeryData) @@ -214,19 +209,25 @@ class ShareViewModel @Inject constructor( }.join() } - private fun getDataByOgTags(it: Document): ContentsSummeryData { - val doc = it.select("meta[property^=og:]") - return ContentsSummeryData(ogUrl = url.value.toString()).apply { - doc.forEachIndexed { index, _ -> - val tag = doc[index] - when (doc[index].attr("property")) { - "og:image" -> ogImage = tag.attr("content") - "og:description" -> ogDescription = tag.attr("content") - "og:title" -> ogTitle = tag.attr("content") - } + private fun setDefaultIfTitleDataNotExist() { + val ogData = ogData.value + if (ogData?.ogTitle.isNullOrBlank()) + ogData?.ogTitle = NO_TITLE_CONTENTS + } + + private fun getDataByOgTags(document: Document): ContentsSummeryData { + val ogTags = document.select("meta[property^=og:]") + val summaryData = ContentsSummeryData(ogUrl = url.value.toString()) + ogTags.forEach { tag -> + val content = tag.attr("content") + when (tag.attr("property")) { + "og:image" -> summaryData.ogImage = content + "og:description" -> summaryData.ogDescription = content + "og:title" -> summaryData.ogTitle = content } - if (this.ogTitle == "") this.ogTitle = it.title() } + if (summaryData.ogTitle.isEmpty()) summaryData.ogTitle = document.title() + return summaryData } private val _saveContentsViewState = MutableLiveData(NetworkStatus.Init()) diff --git a/app/src/main/java/org/sopt/havit/ui/sign/SignInViewModel.kt b/app/src/main/java/org/sopt/havit/ui/sign/SignInViewModel.kt index 9d3e6bc2e..fa4bbab8e 100644 --- a/app/src/main/java/org/sopt/havit/ui/sign/SignInViewModel.kt +++ b/app/src/main/java/org/sopt/havit/ui/sign/SignInViewModel.kt @@ -3,7 +3,6 @@ package org.sopt.havit.ui.sign import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kakao.sdk.auth.model.OAuthToken import dagger.hilt.android.lifecycle.HiltViewModel @@ -11,14 +10,17 @@ import kotlinx.coroutines.launch import org.sopt.havit.data.remote.SignInResponse import org.sopt.havit.domain.entity.NetworkState import org.sopt.havit.domain.repository.AuthRepository +import org.sopt.havit.domain.repository.SystemMaintenanceRepository +import org.sopt.havit.ui.base.BaseViewModel import org.sopt.havit.util.Event import retrofit2.HttpException import javax.inject.Inject @HiltViewModel class SignInViewModel @Inject constructor( - private val authRepository: AuthRepository -) : ViewModel() { + private val authRepository: AuthRepository, + systemMaintenanceRepository: SystemMaintenanceRepository, +) : BaseViewModel(systemMaintenanceRepository) { companion object { const val SPLASH_FROM_SHARE = true diff --git a/app/src/main/java/org/sopt/havit/ui/sign/SplashWithSignActivity.kt b/app/src/main/java/org/sopt/havit/ui/sign/SplashWithSignActivity.kt index 14c12a951..41a84982b 100644 --- a/app/src/main/java/org/sopt/havit/ui/sign/SplashWithSignActivity.kt +++ b/app/src/main/java/org/sopt/havit/ui/sign/SplashWithSignActivity.kt @@ -69,12 +69,14 @@ class SplashWithSignActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(binding.root) this.onBackPressedDispatcher.addCallback(this, callback) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT binding.main = signInViewModel + + signInViewModel.fetchIsSystemMaintenance() + observeSystemUnderMaintenance() initFcmToken() initSuccessKakaoLoginObserver() initWhereSplashComesFrom() @@ -84,6 +86,10 @@ class SplashWithSignActivity : isAlreadyUserObserver() } + private fun observeSystemUnderMaintenance() { + signInViewModel.isSystemMaintenance.observe(this, systemMaintenanceObserver) + } + private fun initFcmToken() { signInViewModel.initFcmToken() } @@ -201,7 +207,7 @@ class SplashWithSignActivity : override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, - grantResults: IntArray + grantResults: IntArray, ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { diff --git a/app/src/main/java/org/sopt/havit/ui/system_maintenance/SystemMaintenanceActivity.kt b/app/src/main/java/org/sopt/havit/ui/system_maintenance/SystemMaintenanceActivity.kt new file mode 100644 index 000000000..2b5b76e14 --- /dev/null +++ b/app/src/main/java/org/sopt/havit/ui/system_maintenance/SystemMaintenanceActivity.kt @@ -0,0 +1,23 @@ +package org.sopt.havit.ui.system_maintenance + +import android.os.Bundle +import androidx.activity.viewModels +import dagger.hilt.android.AndroidEntryPoint +import org.sopt.havit.R +import org.sopt.havit.databinding.ActivitySystemMaintenanceBinding +import org.sopt.havit.ui.base.BaseBindingActivity + +@AndroidEntryPoint +class SystemMaintenanceActivity : + BaseBindingActivity(R.layout.activity_system_maintenance) { + + private val systemMaintenanceViewModel: SystemMaintenanceViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding.viewModel = systemMaintenanceViewModel + + systemMaintenanceViewModel.fetchIsSystemMaintenance() + systemMaintenanceViewModel.fetchSystemMaintenanceMessage() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/havit/ui/system_maintenance/SystemMaintenanceViewModel.kt b/app/src/main/java/org/sopt/havit/ui/system_maintenance/SystemMaintenanceViewModel.kt new file mode 100644 index 000000000..80d0fbc3d --- /dev/null +++ b/app/src/main/java/org/sopt/havit/ui/system_maintenance/SystemMaintenanceViewModel.kt @@ -0,0 +1,36 @@ +package org.sopt.havit.ui.system_maintenance + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.havit.domain.repository.SystemMaintenanceRepository +import javax.inject.Inject + +@HiltViewModel +class SystemMaintenanceViewModel @Inject constructor( + private val systemMaintenanceRepository: SystemMaintenanceRepository, +) : ViewModel() { + + private val _isSystemMaintenance: MutableLiveData = MutableLiveData() + val isSystemMaintenance: LiveData = _isSystemMaintenance + + private var _systemMaintenanceMessage: MutableLiveData = MutableLiveData() + val systemMaintenanceMessage: LiveData = _systemMaintenanceMessage + + fun fetchIsSystemMaintenance() { + viewModelScope.launch { + _isSystemMaintenance.postValue(systemMaintenanceRepository.isSystemMaintenance()) + } + } + + fun fetchSystemMaintenanceMessage() { + viewModelScope.launch { + val message = systemMaintenanceRepository.getSystemMaintenanceMessage() + .replace("\\n", "\n") + _systemMaintenanceMessage.postValue(message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/havit/ui/web/WebActivity.kt b/app/src/main/java/org/sopt/havit/ui/web/WebActivity.kt index d14f0a3f6..b293f69c5 100644 --- a/app/src/main/java/org/sopt/havit/ui/web/WebActivity.kt +++ b/app/src/main/java/org/sopt/havit/ui/web/WebActivity.kt @@ -5,7 +5,11 @@ import android.os.Bundle import android.os.SystemClock import android.view.View.GONE import android.view.animation.AnimationUtils -import android.webkit.* +import android.webkit.URLUtil +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels @@ -37,12 +41,14 @@ class WebActivity : BaseBindingActivity(R.layout.activity_we override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(binding.root) this.onBackPressedDispatcher.addCallback(this, callback) binding.vm = webViewModel startTime = SystemClock.elapsedRealtime().toInt() + + webViewModel.fetchIsSystemMaintenance() + observeSystemUnderMaintenance() initIsHavit() initHavitSeen() setUrlCheck() @@ -77,7 +83,7 @@ class WebActivity : BaseBindingActivity(R.layout.activity_we webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading( view: WebView?, - request: WebResourceRequest? + request: WebResourceRequest?, ): Boolean { if (request?.url.toString().startsWith("towneers:")) { startActivity( @@ -167,4 +173,8 @@ class WebActivity : BaseBindingActivity(R.layout.activity_we setWebViewDurationTimeLogging() GoogleAnalyticsUtil.logClickEvent(CLICK_GO_BACK) } + + private fun observeSystemUnderMaintenance() { + webViewModel.isSystemMaintenance.observe(this, systemMaintenanceObserver) + } } diff --git a/app/src/main/java/org/sopt/havit/ui/web/WebViewModel.kt b/app/src/main/java/org/sopt/havit/ui/web/WebViewModel.kt index 9afbc8689..9869f83cd 100644 --- a/app/src/main/java/org/sopt/havit/ui/web/WebViewModel.kt +++ b/app/src/main/java/org/sopt/havit/ui/web/WebViewModel.kt @@ -2,18 +2,21 @@ package org.sopt.havit.ui.web import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import org.sopt.havit.domain.entity.NetworkState import org.sopt.havit.domain.repository.ContentsRepository +import org.sopt.havit.domain.repository.SystemMaintenanceRepository +import org.sopt.havit.ui.base.BaseViewModel import org.sopt.havit.util.Event import javax.inject.Inject @HiltViewModel -class WebViewModel @Inject constructor(private val contentsRepository: ContentsRepository) : - ViewModel() { +class WebViewModel @Inject constructor( + private val contentsRepository: ContentsRepository, + systemMaintenanceRepository: SystemMaintenanceRepository, +) : BaseViewModel(systemMaintenanceRepository) { private var _isHavit = MutableLiveData>() val isHavit: LiveData> = _isHavit diff --git a/app/src/main/java/org/sopt/havit/util/ToastUtil.kt b/app/src/main/java/org/sopt/havit/util/ToastUtil.kt index 2ea4ed4e7..9e411d1d7 100644 --- a/app/src/main/java/org/sopt/havit/util/ToastUtil.kt +++ b/app/src/main/java/org/sopt/havit/util/ToastUtil.kt @@ -54,6 +54,7 @@ class ToastUtil @Inject constructor(@ApplicationContext private val context: Con val textView: TextView = view.findViewById(R.id.tv_toast) textView.text = categoryName } + else -> { val textView: TextView = view.findViewById(R.id.tv_toast) textView.text = getTitle(context) @@ -78,7 +79,7 @@ enum class ToastCase( @StringRes val text: Int, val viewType: Int, val gravity: Int = Gravity.BOTTOM, - val yOffsetDp: Int = MARGIN_NORMAL + val yOffsetDp: Int = MARGIN_NORMAL, ) { CONTENT_DELETE( R.layout.toast_text, @@ -158,6 +159,11 @@ enum class ToastCase( R.layout.toast_text, R.string.request_delete_notification, REQUEST_DELETE_NOTIFICATION_TYPE + ), + INVALID_URL( + R.layout.toast_text, + R.string.invalid_url, + INVALID_URL_TYPE ); companion object { @@ -182,6 +188,7 @@ const val CATEGORY_MODIFY_COMPLETE_TYPE = 11 const val MODIFY_TITLE_COMPLETE_TYPE = 13 const val DELETE_NOTIFICATION_COMPLETE_TYPE = 14 const val REQUEST_DELETE_NOTIFICATION_TYPE = 15 +const val INVALID_URL_TYPE = 16 const val MARGIN_CONTENT_ADDED = 30 const val MARGIN_HAVIT_COMPLETE = 40 diff --git a/app/src/main/res/drawable/ic_notice.xml b/app/src/main/res/drawable/ic_notice.xml new file mode 100644 index 000000000..6f585faca --- /dev/null +++ b/app/src/main/res/drawable/ic_notice.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/activity_system_maintenance.xml b/app/src/main/res/layout/activity_system_maintenance.xml new file mode 100644 index 000000000..66ff91ee3 --- /dev/null +++ b/app/src/main/res/layout/activity_system_maintenance.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41951d836..5a5cdf766 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -284,5 +284,7 @@ 닉네임에 띄어쓰기는 사용할 수 없습니다. 제목에 띄어쓰기만 사용할 수 없습니다. 닉네임에 띄어쓰기는 사용할 수 없습니다. + 시스템 점검 안내 + 유효하지 않은 URL입니다.