本地播放

This commit is contained in:
ocean 2024-05-27 14:29:44 +08:00
parent 42c95b9a02
commit 2a151145a2
13 changed files with 147 additions and 65 deletions

View File

@ -41,7 +41,7 @@ class LaunchActivity : BaseActivity() {
} }
private fun toMainActivity() { private fun toMainActivity() {
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, PrimaryActivity::class.java))
finish() finish()
} }
} }

View File

@ -14,11 +14,15 @@ import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadManager
import relax.offline.music.R import relax.offline.music.R
import relax.offline.music.media.MediaControllerManager 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.util.LogTag
import relax.offline.music.view.MusicPlayerView import relax.offline.music.view.MusicPlayerView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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(), abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(),
LifecycleOwner { LifecycleOwner {
private var playerListener: Player.Listener? = null private var playerListener: Player.Listener? = null
@ -220,4 +228,18 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
return text.split("\n\n")[0] 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)
}
}
} }

View File

@ -2,18 +2,17 @@ package relax.offline.music.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.View import android.view.View
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.gyf.immersionbar.ktx.immersionBar 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.adapter.OfflineSongsAdapter
import relax.offline.music.bean.OfflineBean import relax.offline.music.bean.OfflineBean
import relax.offline.music.databinding.ActivityOfflineSongsBinding import relax.offline.music.databinding.ActivityOfflineSongsBinding
import relax.offline.music.util.LogTag.LogD 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() { class MoOfflineSongsActivity : MoBaseActivity() {
private val requests: Channel<Request> = Channel(Channel.UNLIMITED) private val requests: Channel<Request> = Channel(Channel.UNLIMITED)
@ -102,12 +101,10 @@ class MoOfflineSongsActivity : MoBaseActivity() {
showLoadingUi() showLoadingUi()
offlineList.clear() offlineList.clear()
offlineList.addAll(relax.offline.music.App.appOfflineDBManager.getAllOfflineBeans()) val offlineBeans = App.appOfflineDBManager.getAllOfflineBeans()
val filteredBeans =
for (offline in offlineList){ offlineBeans.filter { it.bytesDownloaded?.let { bytes -> bytes > 0 } == true }
LogD(TAG,"offline id->${offline.videoId}") offlineList.addAll(filteredBeans)
}
if (offlineList.size > 0) { if (offlineList.size > 0) {
showDataUi() showDataUi()
} else { } else {

View File

@ -41,6 +41,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select import kotlinx.coroutines.selects.select
import relax.offline.music.App
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
@ -61,7 +62,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
private var comeFrom: Class<*>? = null private var comeFrom: Class<*>? = null
private var playListAdapter: PlayListAdapter? = null private var playListAdapter: PlayListAdapter? = null
private var downloadManager: DownloadManager? = null
private fun initImmersionBar() { private fun initImmersionBar() {
immersionBar { immersionBar {
@ -91,15 +91,37 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
if (meController != null && meController.currentMediaItem != null) { if (meController != null && meController.currentMediaItem != null) {
updateInfoUi(meController.currentMediaItem) 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 { } else {
LogD(TAG, "从点击任意歌曲进入") 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.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC) binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
@ -152,11 +174,14 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
LogD(TAG, "initDownloadFlow id ->${id}") LogD(TAG, "initDownloadFlow id ->${id}")
val currentScreenDownloads = downloads[id] val currentScreenDownloads = downloads[id]
if (currentScreenDownloads != null) { if (currentScreenDownloads != null) {
LogD(TAG, "initDownloadFlow Download id->${currentScreenDownloads?.request?.id}") LogD(TAG, "updateDownloadUI id->${currentScreenDownloads.request.id}")
updateDownloadUI(currentScreenDownloads) updateDownloadUI(currentScreenDownloads)
} else {
if (id != null) {
updateDownloadUi(id)
}
} }
} }
} }
} }
@ -203,7 +228,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
binding.downloadBtn.isClickable = false binding.downloadBtn.isClickable = false
binding.downloadBtn.isEnabled = false binding.downloadBtn.isEnabled = false
} else { } else {
binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.download_icon) binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadImg.visibility = View.VISIBLE
binding.downloadBtn.isClickable = true binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true binding.downloadBtn.isEnabled = true
} }
@ -383,6 +410,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
if (DownloadUtil.downloadResourceExist(contentId)) { if (DownloadUtil.downloadResourceExist(contentId)) {
return@setOnClickListener return@setOnClickListener
} }
insertOfflineData(currentMediaItem!!)
val downloadRequest = DownloadRequest val downloadRequest = DownloadRequest
.Builder(contentId, contentId.toUri()) .Builder(contentId, contentId.toUri())
.setCustomCacheKey(contentId) .setCustomCacheKey(contentId)

View File

@ -2,12 +2,15 @@ package relax.offline.music.adapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import relax.offline.music.R 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.bean.OfflineBean
import relax.offline.music.databinding.OfflineListItemBinding import relax.offline.music.databinding.OfflineListItemBinding
import relax.offline.music.media.MediaControllerManager import relax.offline.music.media.MediaControllerManager
@ -28,18 +31,12 @@ class OfflineSongsAdapter(
holder.bind(bean) holder.bind(bean)
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
// val meController = MediaControllerManager.getController() val intent = Intent(context, MoPlayDetailsActivity::class.java)
// if (meController != null && meController.currentMediaItem != null) { intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_VIDEO_ID, bean.videoId)
// var index = holder.bindingAdapterPosition intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.title)
// if (index > meController.mediaItemCount) { intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.name)
// index = 1 intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_COME_FROM, MoOfflineSongsActivity::class.java)
// } context.startActivity(intent)
// meController.seekTo(index, C.TIME_UNSET)
// if (!meController.isPlaying) {
// meController.prepare()
// meController.play()
// }
// }
} }
} }

View File

@ -13,6 +13,7 @@ data class OfflineBean(
@ColumnInfo(name = "title") var title: String, @ColumnInfo(name = "title") var title: String,
@ColumnInfo(name = "name") var name: String, @ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "thumbnail") var thumbnail: String? = null, @ColumnInfo(name = "thumbnail") var thumbnail: String? = null,
@ColumnInfo(name = "bytesDownloaded") var bytesDownloaded: Long? = null,
@ColumnInfo(name = "size") var size: String? = null, @ColumnInfo(name = "size") var size: String? = null,
@ColumnInfo(name = "isOffline") var isOffline: Boolean @ColumnInfo(name = "isOffline") var isOffline: Boolean
) : Serializable { ) : Serializable {

View File

@ -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) return dao.getOfflineBeanByID(id)
} }
} }

View File

@ -4,7 +4,9 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import relax.offline.music.sp.AppStore import relax.offline.music.sp.AppStore
import relax.offline.music.util.LogTag import relax.offline.music.util.LogTag
@ -14,6 +16,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(UnstableApi::class)
abstract class MoBaseFragment<T : ViewBinding> : Fragment(), CoroutineScope by MainScope() { abstract class MoBaseFragment<T : ViewBinding> : Fragment(), CoroutineScope by MainScope() {
enum class Event { enum class Event {
FragmentOnResume, FragmentOnResume,

View File

@ -12,6 +12,7 @@ import relax.offline.music.util.DownloadUtil
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select import kotlinx.coroutines.selects.select
import relax.offline.music.App
class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() { class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
@ -59,7 +60,8 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
} }
binding.offlineSongsBtn.setOnClickListener { 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) val intent = Intent(context, MoOfflineSongsActivity::class.java)
startActivity(intent) startActivity(intent)
} else { } else {
@ -72,8 +74,11 @@ class MoMeFragment : MoBaseFragment<FragmentMoMeBinding>() {
} }
} }
private fun fragmentOnResume() { private suspend fun fragmentOnResume() {
binding.offlineSongsTv.text = "${DownloadUtil.getDownloadCount()}" //过滤只有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() { override fun onResume() {

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import relax.offline.music.App
import java.net.CookieHandler import java.net.CookieHandler
import java.net.CookieManager import java.net.CookieManager
import java.net.CookiePolicy import java.net.CookiePolicy
@ -161,7 +162,7 @@ object DownloadUtil {
.use { cursor -> .use { cursor ->
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val download = cursor.download val download = cursor.download
if (download.request.id == id) { if (download.request.id == id && download.state == Download.STATE_COMPLETED) {
isExist = true isExist = true
} }
} }
@ -245,7 +246,17 @@ object DownloadUtil {
val notification: Notification = when (download.state) { val notification: Notification = when (download.state) {
Download.STATE_COMPLETED -> { 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( notificationHelper.buildDownloadCompletedNotification(
context, context,
@ -271,24 +282,4 @@ object DownloadUtil {
NotificationUtil.setNotification(context, nextNotificationId++, notification) 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)
}
}
}
} }

View File

@ -3,18 +3,35 @@ package relax.offline.music.util
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.annotation.OptIn
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import relax.offline.music.bean.OfflineBean
import relax.offline.music.innertube.Innertube import relax.offline.music.innertube.Innertube
import relax.offline.music.innertube.models.bodies.ContinuationBody import relax.offline.music.innertube.models.bodies.ContinuationBody
import relax.offline.music.innertube.requests.playlistPage import relax.offline.music.innertube.requests.playlistPage
import relax.offline.music.innertube.utils.plus 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 val Innertube.SongItem.asMediaItem: MediaItem
@UnstableApi @OptIn(UnstableApi::class)
get() = MediaItem.Builder() get() = MediaItem.Builder()
.setMediaId(key) .setMediaId(key)
.setUri(key) .setUri(key)
@ -29,7 +46,8 @@ val Innertube.SongItem.asMediaItem: MediaItem
bundleOf( bundleOf(
"albumId" to album?.endpoint?.browseId, "albumId" to album?.endpoint?.browseId,
"durationText" to durationText, "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 }, "artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
) )
) )
@ -56,7 +74,8 @@ suspend fun Result<Innertube.PlaylistOrAlbumPage>.completed(): Result<Innertube.
while (playlistPage.songsPage?.continuation != null) { while (playlistPage.songsPage?.continuation != null) {
val continuation = playlistPage.songsPage?.continuation!! 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 if (otherPlaylistPageResult.isFailure) break

View 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>

View File

@ -94,13 +94,14 @@
<RelativeLayout <RelativeLayout
android:id="@+id/downloadBtn" android:id="@+id/downloadBtn"
android:layout_width="40dp" android:layout_width="40dp"
android:visibility="gone"
android:layout_height="40dp"> android:layout_height="40dp">
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:src="@drawable/download_icon" /> android:src="@drawable/download_gray_icon" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>