feat: 마이페이지
- 최근 들은 콘텐츠 추가
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -56,6 +54,8 @@ import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentTempActivity
|
||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
||||
import kr.co.vividnext.sodalive.report.ReportType
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.ceil
|
||||
@@ -65,6 +65,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
ActivityAudioContentDetailBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentDetailViewModel by inject()
|
||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var creatorOtherContentAdapter: OtherContentAdapter
|
||||
@@ -808,6 +809,15 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
recentContentViewModel.insertRecentContent(
|
||||
RecentContent(
|
||||
contentId = response.contentId,
|
||||
coverImageUrl = response.coverImageUrl,
|
||||
title = response.title,
|
||||
creatorNickname = response.creator.nickname
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
|
||||
@@ -40,6 +40,9 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentPlayerBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
@UnstableApi
|
||||
@@ -53,6 +56,7 @@ class AudioContentPlayerFragment(
|
||||
private lateinit var binding: FragmentAudioContentPlayerBinding
|
||||
|
||||
private val viewModel: AudioContentPlayerViewModel by viewModel()
|
||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||
|
||||
private var mediaController: MediaController? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
@@ -451,7 +455,19 @@ class AudioContentPlayerFragment(
|
||||
transformations(RoundedCornersTransformation(8f.dpToPx()))
|
||||
}
|
||||
|
||||
adapter.updateCurrentPlayingId(it.extras?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID))
|
||||
val contentId = it.extras?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
|
||||
adapter.updateCurrentPlayingId(contentId)
|
||||
|
||||
// Save to recent content
|
||||
contentId?.let { id ->
|
||||
val recentContent = RecentContent(
|
||||
contentId = id,
|
||||
coverImageUrl = it.artworkUri.toString(),
|
||||
title = it.title.toString(),
|
||||
creatorNickname = it.artist.toString()
|
||||
)
|
||||
recentContentViewModel.insertRecentContent(recentContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagApi
|
||||
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagRepository
|
||||
import kr.co.vividnext.sodalive.mypage.profile.tag.MemberTagViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.recent.recentContentModule
|
||||
import kr.co.vividnext.sodalive.mypage.service_center.FaqApi
|
||||
import kr.co.vividnext.sodalive.mypage.service_center.FaqRepository
|
||||
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel
|
||||
@@ -387,10 +388,12 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
factory { HomeRepository(get()) }
|
||||
}
|
||||
|
||||
|
||||
private val moduleList = listOf(
|
||||
networkModule,
|
||||
viewModelModule,
|
||||
repositoryModule,
|
||||
recentContentModule,
|
||||
otherModule
|
||||
)
|
||||
|
||||
|
||||
@@ -2,23 +2,28 @@ package kr.co.vividnext.sodalive.mypage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.google.gson.Gson
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.FunctionButtonHelper
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentMyBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.main.MainActivity
|
||||
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity
|
||||
@@ -31,6 +36,8 @@ import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponActivity
|
||||
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
|
||||
import kr.co.vividnext.sodalive.mypage.point.PointStatusActivity
|
||||
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateActivity
|
||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentAdapter
|
||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterActivity
|
||||
import kr.co.vividnext.sodalive.settings.SettingsActivity
|
||||
import kr.co.vividnext.sodalive.settings.event.EventActivity
|
||||
@@ -42,6 +49,7 @@ import org.koin.android.ext.android.inject
|
||||
class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflate) {
|
||||
|
||||
private val viewModel: MyPageViewModel by inject()
|
||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
@@ -51,6 +59,73 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
||||
|
||||
setupView()
|
||||
bindData()
|
||||
setupRecentContentSection()
|
||||
}
|
||||
|
||||
private fun setupRecentContentSection() {
|
||||
val adapter = RecentContentAdapter {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
AudioContentDetailActivity::class.java
|
||||
).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val rvRecentContent = binding.rvRecentContent
|
||||
|
||||
rvRecentContent.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
rvRecentContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> {
|
||||
outRect.left = 0
|
||||
outRect.right = 8f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.left = 8f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 8f.dpToPx().toInt()
|
||||
outRect.right = 8f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
rvRecentContent.adapter = adapter
|
||||
|
||||
// Observe recent contents
|
||||
recentContentViewModel.getRecentContents(10).observe(viewLifecycleOwner) { contents ->
|
||||
if (contents.isNotEmpty()) {
|
||||
binding.llRecentContent.visibility = View.VISIBLE
|
||||
adapter.submitList(contents)
|
||||
} else {
|
||||
binding.llRecentContent.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
// Observe count
|
||||
recentContentViewModel.recentContentsCount.observe(viewLifecycleOwner) { count ->
|
||||
binding.tvRecentCount.text = count.toString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -62,10 +137,6 @@ class MyPageFragment : BaseFragment<FragmentMyBinding>(FragmentMyBinding::inflat
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
// val ivHowToUseLp = binding.ivIntroduceVoiceon.layoutParams as LinearLayout.LayoutParams
|
||||
// ivHowToUseLp.width = screenWidth
|
||||
// ivHowToUseLp.height = (120 * screenWidth) / 352
|
||||
// binding.ivIntroduceVoiceon.layoutParams = ivHowToUseLp
|
||||
binding.ivIntroduceVoiceon.setOnClickListener {
|
||||
val url = "https://blog.naver.com/sodalive_official"
|
||||
if (URLUtil.isValidUrl(url)) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package kr.co.vividnext.sodalive.mypage.recent
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemHomeContentBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
||||
|
||||
class RecentContentAdapter(
|
||||
private val onClickItem: (Long) -> Unit
|
||||
) : ListAdapter<RecentContent, RecentContentAdapter.RecentContentViewHolder>(
|
||||
RecentContentDiffCallback()
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentContentViewHolder {
|
||||
val binding = ItemHomeContentBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return RecentContentViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecentContentViewHolder, position: Int) {
|
||||
val currentItem = getItem(position)
|
||||
holder.bind(currentItem)
|
||||
}
|
||||
|
||||
inner class RecentContentViewHolder(
|
||||
private val binding: ItemHomeContentBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: RecentContent) {
|
||||
binding.ivPoint.visibility = View.GONE
|
||||
|
||||
binding.ivContentCoverImage.load(item.coverImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(RoundedCornersTransformation(16f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvContentTitle.text = item.title
|
||||
binding.tvNickname.text = item.creatorNickname
|
||||
|
||||
binding.root.setOnClickListener { onClickItem(item.contentId) }
|
||||
}
|
||||
}
|
||||
|
||||
class RecentContentDiffCallback : DiffUtil.ItemCallback<RecentContent>() {
|
||||
override fun areItemsTheSame(oldItem: RecentContent, newItem: RecentContent): Boolean {
|
||||
return oldItem.contentId == newItem.contentId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: RecentContent, newItem: RecentContent): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package kr.co.vividnext.sodalive.mypage.recent
|
||||
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContentDatabase
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val recentContentModule = module {
|
||||
// Database
|
||||
single { RecentContentDatabase.getDatabase(androidContext()) }
|
||||
|
||||
// DAO
|
||||
single { get<RecentContentDatabase>().recentContentDao() }
|
||||
|
||||
// Repository
|
||||
factory { RecentContentRepository(get()) }
|
||||
|
||||
// ViewModel
|
||||
viewModel { RecentContentViewModel(get()) }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package kr.co.vividnext.sodalive.mypage.recent
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContentDao
|
||||
|
||||
class RecentContentRepository(private val recentContentDao: RecentContentDao) {
|
||||
val recentContentsCount: Flow<Int> = recentContentDao.getCount()
|
||||
|
||||
fun getRecentContents(limit: Int): Flow<List<RecentContent>> {
|
||||
return recentContentDao.getRecentContents(limit)
|
||||
}
|
||||
|
||||
suspend fun insertRecentContent(recentContent: RecentContent) {
|
||||
recentContentDao.insertRecentContent(recentContent)
|
||||
// Keep only the most recent 10 items
|
||||
recentContentDao.keepMostRecent(10)
|
||||
}
|
||||
|
||||
suspend fun deleteByContentId(contentId: Long) {
|
||||
recentContentDao.deleteByContentId(contentId)
|
||||
}
|
||||
|
||||
suspend fun truncate() {
|
||||
recentContentDao.deleteAllRecentContents()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package kr.co.vividnext.sodalive.mypage.recent
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
||||
|
||||
class RecentContentViewModel(private val repository: RecentContentRepository) : ViewModel() {
|
||||
val recentContentsCount: LiveData<Int> = repository.recentContentsCount.asLiveData()
|
||||
|
||||
fun getRecentContents(limit: Int): LiveData<List<RecentContent>> {
|
||||
return repository.getRecentContents(limit).asLiveData()
|
||||
}
|
||||
|
||||
fun insertRecentContent(recentContent: RecentContent) = viewModelScope.launch {
|
||||
repository.insertRecentContent(recentContent)
|
||||
}
|
||||
|
||||
fun deleteByContentId(contentId: Long) = viewModelScope.launch {
|
||||
repository.deleteByContentId(contentId)
|
||||
}
|
||||
|
||||
fun truncate() = viewModelScope.launch {
|
||||
repository.truncate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.mypage.recent.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "recent_contents")
|
||||
data class RecentContent(
|
||||
@PrimaryKey
|
||||
val contentId: Long,
|
||||
val coverImageUrl: String,
|
||||
val title: String,
|
||||
val creatorNickname: String,
|
||||
val listenedAt: Long = System.currentTimeMillis() // For sorting by most recent
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
package kr.co.vividnext.sodalive.mypage.recent.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RecentContentDao {
|
||||
@Query("SELECT * FROM recent_contents ORDER BY listenedAt DESC LIMIT :limit")
|
||||
fun getRecentContents(limit: Int): Flow<List<RecentContent>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecentContent(recentContent: RecentContent)
|
||||
|
||||
@Query("DELETE FROM recent_contents WHERE contentId = :contentId")
|
||||
suspend fun deleteByContentId(contentId: Long)
|
||||
|
||||
@Query("SELECT COUNT(*) FROM recent_contents")
|
||||
fun getCount(): Flow<Int>
|
||||
|
||||
@Query("DELETE FROM recent_contents WHERE contentId NOT IN (SELECT contentId FROM recent_contents ORDER BY listenedAt DESC LIMIT :limit)")
|
||||
suspend fun keepMostRecent(limit: Int)
|
||||
|
||||
@Query("DELETE FROM recent_contents")
|
||||
suspend fun deleteAllRecentContents()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package kr.co.vividnext.sodalive.mypage.recent.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import kr.co.vividnext.sodalive.common.Converter
|
||||
|
||||
@Database(entities = [RecentContent::class], version = 1)
|
||||
@TypeConverters(Converter::class)
|
||||
abstract class RecentContentDatabase : RoomDatabase() {
|
||||
abstract fun recentContentDao(): RecentContentDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: RecentContentDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): RecentContentDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
RecentContentDatabase::class.java,
|
||||
"recent_content_database" // Different name from alarm database
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivitySettingsBinding
|
||||
import kr.co.vividnext.sodalive.mypage.alarm.AlarmViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
|
||||
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsActivity
|
||||
import kr.co.vividnext.sodalive.settings.signout.SignOutActivity
|
||||
import kr.co.vividnext.sodalive.settings.terms.TermsActivity
|
||||
@@ -56,6 +57,7 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>(ActivitySettingsB
|
||||
|
||||
private val viewModel: SettingsViewModel by inject()
|
||||
private val alarmViewModel: AlarmViewModel by viewModels()
|
||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
@@ -158,8 +160,11 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>(ActivitySettingsB
|
||||
|
||||
viewModel.logout {
|
||||
NotiflyClient.logout(context = applicationContext)
|
||||
|
||||
SharedPreferenceManager.clear()
|
||||
alarmViewModel.truncate()
|
||||
recentContentViewModel.truncate()
|
||||
|
||||
finishAffinity()
|
||||
startActivity(Intent(applicationContext, SplashActivity::class.java))
|
||||
}
|
||||
@@ -181,9 +186,12 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>(ActivitySettingsB
|
||||
|
||||
viewModel.logoutAllDevice() {
|
||||
NotiflyClient.logout(context = applicationContext)
|
||||
|
||||
SharedPreferenceManager.clear()
|
||||
alarmViewModel.truncate()
|
||||
finishAffinity()
|
||||
recentContentViewModel.truncate()
|
||||
|
||||
finishAffinity()
|
||||
startActivity(Intent(applicationContext, SplashActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,9 +385,7 @@
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_recent_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="24dp" />
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
Reference in New Issue
Block a user