本地播放
This commit is contained in:
parent
42c95b9a02
commit
2a151145a2
@ -41,7 +41,7 @@ class LaunchActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun toMainActivity() {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
startActivity(Intent(this, PrimaryActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@ -14,11 +14,15 @@ import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import relax.offline.music.R
|
||||
import relax.offline.music.media.MediaControllerManager
|
||||
@ -26,13 +30,17 @@ import relax.offline.music.sp.AppStore
|
||||
import relax.offline.music.util.LogTag
|
||||
import relax.offline.music.view.MusicPlayerView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import relax.offline.music.bean.OfflineBean
|
||||
import relax.offline.music.innertube.Innertube
|
||||
import relax.offline.music.util.FileSizeConverter
|
||||
|
||||
@UnstableApi
|
||||
@OptIn(UnstableApi::class)
|
||||
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(),
|
||||
LifecycleOwner {
|
||||
private var playerListener: Player.Listener? = null
|
||||
@ -220,4 +228,18 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
|
||||
return text.split("\n\n")[0]
|
||||
}
|
||||
|
||||
|
||||
fun insertOfflineData(mediaItem: MediaItem) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val bean = OfflineBean(
|
||||
videoId = mediaItem.mediaId,
|
||||
title = mediaItem.mediaMetadata.title.toString(),
|
||||
name = mediaItem.mediaMetadata.artist.toString(),
|
||||
thumbnail = mediaItem.mediaMetadata.artworkUri.toString(),
|
||||
isOffline = true
|
||||
)
|
||||
LogTag.LogD(Innertube.TAG, "insertOfflineBean bean->${bean}")
|
||||
relax.offline.music.App.appOfflineDBManager.insertOfflineBean(bean)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,17 @@ package relax.offline.music.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.gyf.immersionbar.ktx.immersionBar
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.selects.select
|
||||
import relax.offline.music.App
|
||||
import relax.offline.music.adapter.OfflineSongsAdapter
|
||||
import relax.offline.music.bean.OfflineBean
|
||||
import relax.offline.music.databinding.ActivityOfflineSongsBinding
|
||||
import relax.offline.music.util.LogTag.LogD
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.selects.select
|
||||
|
||||
@UnstableApi
|
||||
class MoOfflineSongsActivity : MoBaseActivity() {
|
||||
private val requests: Channel<Request> = Channel(Channel.UNLIMITED)
|
||||
|
||||
@ -102,12 +101,10 @@ class MoOfflineSongsActivity : MoBaseActivity() {
|
||||
showLoadingUi()
|
||||
|
||||
offlineList.clear()
|
||||
offlineList.addAll(relax.offline.music.App.appOfflineDBManager.getAllOfflineBeans())
|
||||
|
||||
for (offline in offlineList){
|
||||
LogD(TAG,"offline id->${offline.videoId}")
|
||||
}
|
||||
|
||||
val offlineBeans = App.appOfflineDBManager.getAllOfflineBeans()
|
||||
val filteredBeans =
|
||||
offlineBeans.filter { it.bytesDownloaded?.let { bytes -> bytes > 0 } == true }
|
||||
offlineList.addAll(filteredBeans)
|
||||
if (offlineList.size > 0) {
|
||||
showDataUi()
|
||||
} else {
|
||||
|
||||
@ -41,6 +41,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import relax.offline.music.App
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
@ -61,7 +62,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
|
||||
private var comeFrom: Class<*>? = null
|
||||
private var playListAdapter: PlayListAdapter? = null
|
||||
private var downloadManager: DownloadManager? = null
|
||||
|
||||
private fun initImmersionBar() {
|
||||
immersionBar {
|
||||
@ -91,15 +91,37 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
if (meController != null && meController.currentMediaItem != null) {
|
||||
updateInfoUi(meController.currentMediaItem)
|
||||
}
|
||||
} else if (comeFrom != null && comeFrom == MoOfflineSongsActivity::class.java) {
|
||||
LogD(TAG, "从offline songs 进入")
|
||||
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
|
||||
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
|
||||
//从数据库获取所有的offline
|
||||
val offlineBeans = App.appOfflineDBManager.getAllOfflineBeans()
|
||||
//过滤只有大小大于0的才添加到集合中
|
||||
val allFilteredBeans = offlineBeans
|
||||
.filter { it.bytesDownloaded?.let { bytes -> bytes > 0 } == true }
|
||||
//找到当前点击进来的歌曲media
|
||||
val findCurrentMedia = allFilteredBeans.find { it.videoId == videoId }?.asMediaItem
|
||||
if (findCurrentMedia != null) {
|
||||
binding.likeAndDownloadLayout.visibility = View.VISIBLE
|
||||
updateInfoUi(findCurrentMedia)
|
||||
binding.playbackErrorLayout.visibility = View.GONE
|
||||
binding.totalDurationTv.visibility = View.GONE
|
||||
meController?.let {
|
||||
it.setMediaItem(findCurrentMedia, true)
|
||||
it.prepare()
|
||||
it.play()
|
||||
val mediaItems = allFilteredBeans
|
||||
.map { mapAll -> mapAll.asMediaItem }//转换成MediaItem
|
||||
.filter { filter -> filter.mediaId != videoId }//过滤掉id相等的。
|
||||
it.addMediaItems(mediaItems)
|
||||
}
|
||||
updatePlayListDataAndAdapter()
|
||||
} else {
|
||||
binding.playbackErrorLayout.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
LogD(TAG, "从点击任意歌曲进入")
|
||||
// if (meController != null && meController.currentMediaItem != null && videoId == meController.currentMediaItem?.mediaId) {
|
||||
// //进入的id与当前的id一样就不重新去获取播放
|
||||
// updateCurrentMediaItemInfo()
|
||||
// updateInfoUi(meController.currentMediaItem)
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
|
||||
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
|
||||
|
||||
@ -152,11 +174,14 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
LogD(TAG, "initDownloadFlow id ->${id}")
|
||||
val currentScreenDownloads = downloads[id]
|
||||
if (currentScreenDownloads != null) {
|
||||
LogD(TAG, "initDownloadFlow Download id->${currentScreenDownloads?.request?.id}")
|
||||
LogD(TAG, "updateDownloadUI id->${currentScreenDownloads.request.id}")
|
||||
updateDownloadUI(currentScreenDownloads)
|
||||
} else {
|
||||
if (id != null) {
|
||||
updateDownloadUi(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,7 +228,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
binding.downloadBtn.isClickable = false
|
||||
binding.downloadBtn.isEnabled = false
|
||||
} else {
|
||||
binding.downloadLoading.visibility = View.GONE
|
||||
binding.downloadImg.setImageResource(R.drawable.download_icon)
|
||||
binding.downloadImg.visibility = View.VISIBLE
|
||||
binding.downloadBtn.isClickable = true
|
||||
binding.downloadBtn.isEnabled = true
|
||||
}
|
||||
@ -383,6 +410,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
if (DownloadUtil.downloadResourceExist(contentId)) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
insertOfflineData(currentMediaItem!!)
|
||||
|
||||
val downloadRequest = DownloadRequest
|
||||
.Builder(contentId, contentId.toUri())
|
||||
.setCustomCacheKey(contentId)
|
||||
|
||||
@ -2,12 +2,15 @@ package relax.offline.music.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import relax.offline.music.R
|
||||
import relax.offline.music.activity.MoOfflineSongsActivity
|
||||
import relax.offline.music.activity.MoPlayDetailsActivity
|
||||
import relax.offline.music.bean.OfflineBean
|
||||
import relax.offline.music.databinding.OfflineListItemBinding
|
||||
import relax.offline.music.media.MediaControllerManager
|
||||
@ -28,18 +31,12 @@ class OfflineSongsAdapter(
|
||||
holder.bind(bean)
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
// val meController = MediaControllerManager.getController()
|
||||
// if (meController != null && meController.currentMediaItem != null) {
|
||||
// var index = holder.bindingAdapterPosition
|
||||
// if (index > meController.mediaItemCount) {
|
||||
// index = 1
|
||||
// }
|
||||
// meController.seekTo(index, C.TIME_UNSET)
|
||||
// if (!meController.isPlaying) {
|
||||
// meController.prepare()
|
||||
// meController.play()
|
||||
// }
|
||||
// }
|
||||
val intent = Intent(context, MoPlayDetailsActivity::class.java)
|
||||
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_VIDEO_ID, bean.videoId)
|
||||
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.title)
|
||||
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.name)
|
||||
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_COME_FROM, MoOfflineSongsActivity::class.java)
|
||||
context.startActivity(intent)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ data class OfflineBean(
|
||||
@ColumnInfo(name = "title") var title: String,
|
||||
@ColumnInfo(name = "name") var name: String,
|
||||
@ColumnInfo(name = "thumbnail") var thumbnail: String? = null,
|
||||
@ColumnInfo(name = "bytesDownloaded") var bytesDownloaded: Long? = null,
|
||||
@ColumnInfo(name = "size") var size: String? = null,
|
||||
@ColumnInfo(name = "isOffline") var isOffline: Boolean
|
||||
) : Serializable {
|
||||
|
||||
@ -77,7 +77,7 @@ class AppOfflineDBManager private constructor(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOfflineBeanByID(id: String): OfflineBean? {
|
||||
suspend fun getOfflineBeanByID(id: String): OfflineBean? {
|
||||
return dao.getOfflineBeanByID(id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,9 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import relax.offline.music.sp.AppStore
|
||||
import relax.offline.music.util.LogTag
|
||||
@ -14,6 +16,7 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
abstract class MoBaseFragment<T : ViewBinding> : Fragment(), CoroutineScope by MainScope() {
|
||||
enum class Event {
|
||||
FragmentOnResume,
|
||||
|
||||
@ -12,6 +12,7 @@ import relax.offline.music.util.DownloadUtil
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.selects.select
|
||||
import relax.offline.music.App
|
||||
|
||||
class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
|
||||
|
||||
@ -59,7 +60,8 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
|
||||
|
||||
}
|
||||
binding.offlineSongsBtn.setOnClickListener {
|
||||
if (DownloadUtil.getDownloadCount() > 0) {
|
||||
val count = binding.offlineSongsTv.text.toString().trim().toInt()
|
||||
if (count > 0) {
|
||||
val intent = Intent(context, MoOfflineSongsActivity::class.java)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
@ -72,8 +74,11 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun fragmentOnResume() {
|
||||
binding.offlineSongsTv.text = "${DownloadUtil.getDownloadCount()}"
|
||||
private suspend fun fragmentOnResume() {
|
||||
//过滤只有size大于0的才计数
|
||||
val offlineBeans = App.appOfflineDBManager.getAllOfflineBeans()
|
||||
val count = offlineBeans.count { it.bytesDownloaded?.let { bytes -> bytes > 0 } == true }
|
||||
binding.offlineSongsTv.text = "$count"
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@ -37,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import relax.offline.music.App
|
||||
import java.net.CookieHandler
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
@ -161,7 +162,7 @@ object DownloadUtil {
|
||||
.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val download = cursor.download
|
||||
if (download.request.id == id) {
|
||||
if (download.request.id == id && download.state == Download.STATE_COMPLETED) {
|
||||
isExist = true
|
||||
}
|
||||
}
|
||||
@ -245,7 +246,17 @@ object DownloadUtil {
|
||||
|
||||
val notification: Notification = when (download.state) {
|
||||
Download.STATE_COMPLETED -> {
|
||||
insertOfflineData(download)
|
||||
//更新大小
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val offlineBean =
|
||||
App.appOfflineDBManager.getOfflineBeanByID(download.request.id)
|
||||
if (offlineBean != null) {
|
||||
offlineBean.size =
|
||||
FileSizeConverter(download.bytesDownloaded).formattedSize()
|
||||
offlineBean.bytesDownloaded = download.bytesDownloaded
|
||||
App.appOfflineDBManager.updateOfflineBean(offlineBean)
|
||||
}
|
||||
}
|
||||
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
context,
|
||||
@ -271,24 +282,4 @@ object DownloadUtil {
|
||||
NotificationUtil.setNotification(context, nextNotificationId++, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertOfflineData(download: Download) {
|
||||
val meController = MediaControllerManager.getController()
|
||||
if (meController != null && meController.currentMediaItem != null) {
|
||||
val currentMediaItem = meController.currentMediaItem
|
||||
val mediaMetadata = currentMediaItem?.mediaMetadata
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val bean = OfflineBean(
|
||||
videoId = download.request.id,
|
||||
title = mediaMetadata?.title.toString(),
|
||||
name = mediaMetadata?.artist.toString(),
|
||||
thumbnail = mediaMetadata?.artworkUri.toString(),
|
||||
size = FileSizeConverter(download.bytesDownloaded).formattedSize(),
|
||||
isOffline = true
|
||||
)
|
||||
LogTag.LogD(TAG, "insertOfflineBean bean->${bean}")
|
||||
relax.offline.music.App.appOfflineDBManager.insertOfflineBean(bean)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,18 +3,35 @@ package relax.offline.music.util
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.format.DateUtils
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import relax.offline.music.bean.OfflineBean
|
||||
import relax.offline.music.innertube.Innertube
|
||||
import relax.offline.music.innertube.models.bodies.ContinuationBody
|
||||
import relax.offline.music.innertube.requests.playlistPage
|
||||
import relax.offline.music.innertube.utils.plus
|
||||
|
||||
val OfflineBean.asMediaItem: MediaItem
|
||||
@OptIn(UnstableApi::class)
|
||||
get() = MediaItem.Builder()
|
||||
.setMediaId(videoId)
|
||||
.setUri(videoId)
|
||||
.setCustomCacheKey(videoId)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
.setArtist(name)
|
||||
.setArtworkUri(thumbnail?.toUri())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
val Innertube.SongItem.asMediaItem: MediaItem
|
||||
@UnstableApi
|
||||
@OptIn(UnstableApi::class)
|
||||
get() = MediaItem.Builder()
|
||||
.setMediaId(key)
|
||||
.setUri(key)
|
||||
@ -29,7 +46,8 @@ val Innertube.SongItem.asMediaItem: MediaItem
|
||||
bundleOf(
|
||||
"albumId" to album?.endpoint?.browseId,
|
||||
"durationText" to durationText,
|
||||
"artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name },
|
||||
"artistNames" to authors?.filter { it.endpoint != null }
|
||||
?.mapNotNull { it.name },
|
||||
"artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
|
||||
)
|
||||
)
|
||||
@ -56,7 +74,8 @@ suspend fun Result<Innertube.PlaylistOrAlbumPage>.completed(): Result<Innertube.
|
||||
|
||||
while (playlistPage.songsPage?.continuation != null) {
|
||||
val continuation = playlistPage.songsPage?.continuation!!
|
||||
val otherPlaylistPageResult = Innertube.playlistPage(ContinuationBody(continuation = continuation)) ?: break
|
||||
val otherPlaylistPageResult =
|
||||
Innertube.playlistPage(ContinuationBody(continuation = continuation)) ?: break
|
||||
|
||||
if (otherPlaylistPageResult.isFailure) break
|
||||
|
||||
|
||||
16
app/src/main/res/drawable/download_gray_icon.xml
Normal file
16
app/src/main/res/drawable/download_gray_icon.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF666666"
|
||||
android:pathData="M2.623,8.779C3.339,5.724 5.724,3.339 8.779,2.623C10.898,2.126 13.102,2.126 15.221,2.623C18.276,3.339 20.661,5.724 21.377,8.779C21.874,10.898 21.874,13.102 21.377,15.221C20.661,18.276 18.276,20.661 15.221,21.377C13.102,21.874 10.898,21.874 8.779,21.377C5.724,20.661 3.339,18.276 2.623,15.221C2.126,13.102 2.126,10.898 2.623,8.779L4.083,9.122C3.639,11.015 3.639,12.985 4.083,14.878C4.669,17.378 6.622,19.33 9.122,19.917C11.015,20.361 12.985,20.361 14.878,19.917C17.378,19.33 19.33,17.378 19.917,14.878C20.361,12.985 20.361,11.015 19.917,9.122C19.33,6.622 17.378,4.669 14.878,4.083C12.985,3.639 11.015,3.639 9.122,4.083C6.622,4.669 4.669,6.622 4.083,9.122L2.623,8.779Z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,8.5V15.5M12,15.5L14.5,13M12,15.5L9.5,13"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#FF666666"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
@ -94,13 +94,14 @@
|
||||
<RelativeLayout
|
||||
android:id="@+id/downloadBtn"
|
||||
android:layout_width="40dp"
|
||||
android:visibility="gone"
|
||||
android:layout_height="40dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:src="@drawable/download_icon" />
|
||||
android:src="@drawable/download_gray_icon" />
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user