Remove Internet permission along with all Internet stuff

Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
This commit is contained in:
Muntashir Al-Islam 2022-05-25 15:11:55 +06:00
parent b103387d73
commit 12bded682c
34 changed files with 18 additions and 2010 deletions

View file

@ -88,11 +88,7 @@ dependencies {
implementation "androidx.core:core-splashscreen:1.0.0-beta02"
implementation "com.google.android.material:material:$mdc_version"
def retrofit_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.7'
implementation 'com.google.code.gson:gson:2.9.0'
def material_dialog_version = "3.3.0"
implementation "com.afollestad.material-dialogs:core:$material_dialog_version"
@ -114,7 +110,6 @@ dependencies {
def glide_version = '4.13.2'
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
@ -125,7 +120,6 @@ dependencies {
implementation "dev.chrisbanes.insetter:insetter:0.6.1"
implementation 'org.eclipse.mylyn.github:org.eclipse.egit.github.core:2.1.5'
implementation 'com.github.Adonai:jaudiotagger:2.3.15'
implementation 'com.r0adkll:slidableactivity:2.1.0'
implementation 'com.heinrichreimersoftware:material-intro:2.0.0'

View file

@ -15,8 +15,6 @@
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission

View file

@ -18,20 +18,12 @@ import android.provider.BaseColumns
import android.provider.MediaStore
object Constants {
const val RATE_ON_GOOGLE_PLAY =
"https://play.google.com/store/apps/details?id=code.name.monkey.retromusic"
const val TRANSLATE = "https://crowdin.com/project/retromusicplayer"
const val WEBSITE = "https://retromusic.app"
const val GITHUB_PROJECT = "https://github.com/MuntashirAkon/Metro"
const val TELEGRAM_CHANGE_LOG = "https://t.me/AppManagerChannel"
const val USER_PROFILE = "profile.jpg"
const val USER_BANNER = "banner.jpg"
const val APP_INSTAGRAM_LINK = "https://www.instagram.com/retromusicapp/"
const val APP_TELEGRAM_LINK = "https://t.me/retromusicapp/"
const val APP_TWITTER_LINK = "https://twitter.com/retromusicapp"
const val FAQ_LINK = "https://github.com/MuntashirAkon/Metro/blob/master/FAQ.md"
const val PINTEREST = "https://in.pinterest.com/retromusicapp/"
const val AUDIO_SCROBBLER_URL = "https://ws.audioscrobbler.com/2.0/"
const val IS_MUSIC =
MediaStore.Audio.AudioColumns.IS_MUSIC + "=1" + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"
@ -130,7 +122,6 @@ const val LAST_SLEEP_TIMER_VALUE = "last_sleep_timer_value"
const val NEXT_SLEEP_TIMER_ELAPSED_REALTIME = "next_sleep_timer_elapsed_real_time"
const val IGNORE_MEDIA_STORE_ARTWORK = "ignore_media_store_artwork"
const val LAST_CHANGELOG_VERSION = "last_changelog_version"
const val AUTO_DOWNLOAD_IMAGES_POLICY = "auto_download_images_policy"
const val START_DIRECTORY = "start_directory"
const val RECENTLY_PLAYED_CUTOFF = "recently_played_interval"
const val LOCK_SCREEN = "lock_screen"

View file

@ -14,10 +14,6 @@ import io.github.muntashirakon.music.fragments.artists.ArtistDetailsViewModel
import io.github.muntashirakon.music.fragments.genres.GenreDetailsViewModel
import io.github.muntashirakon.music.fragments.playlists.PlaylistDetailsViewModel
import io.github.muntashirakon.music.model.Genre
import io.github.muntashirakon.music.network.provideDefaultCache
import io.github.muntashirakon.music.network.provideLastFmRest
import io.github.muntashirakon.music.network.provideLastFmRetrofit
import io.github.muntashirakon.music.network.provideOkHttp
import io.github.muntashirakon.music.repository.*
import io.github.muntashirakon.music.util.FilePathUtil
import kotlinx.coroutines.Dispatchers.IO
@ -28,22 +24,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module
val networkModule = module {
factory {
provideDefaultCache()
}
factory {
provideOkHttp(get(), get())
}
single {
provideLastFmRetrofit(get())
}
single {
provideLastFmRest(get())
}
}
private val roomModule = module {
single {
@ -117,7 +97,6 @@ private val dataModule = module {
get(),
get(),
get(),
get(),
)
} bind Repository::class
@ -200,4 +179,4 @@ private val viewModules = module {
}
}
val appModules = listOf(mainModule, dataModule, autoModule, viewModules, networkModule, roomModule)
val appModules = listOf(mainModule, dataModule, autoModule, viewModules, roomModule)

View file

@ -19,53 +19,17 @@ import android.content.ClipboardManager
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.inputmethod.EditorInfo
import androidx.annotation.StringDef
import androidx.annotation.StringRes
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import code.name.monkey.appthemehelper.util.MaterialUtil
import code.name.monkey.appthemehelper.util.TintHelper
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.base.AbsThemeActivity
import io.github.muntashirakon.music.activities.bugreport.model.DeviceInfo
import io.github.muntashirakon.music.activities.bugreport.model.Report
import io.github.muntashirakon.music.activities.bugreport.model.github.ExtraInfo
import io.github.muntashirakon.music.activities.bugreport.model.github.GithubLogin
import io.github.muntashirakon.music.activities.bugreport.model.github.GithubTarget
import io.github.muntashirakon.music.databinding.ActivityBugReportBinding
import io.github.muntashirakon.music.extensions.accentColor
import io.github.muntashirakon.music.extensions.setTaskDescriptionColorAuto
import io.github.muntashirakon.music.extensions.showToast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.egit.github.core.Issue
import org.eclipse.egit.github.core.client.GitHubClient
import org.eclipse.egit.github.core.client.RequestException
import org.eclipse.egit.github.core.service.IssueService
import java.io.IOException
private const val RESULT_SUCCESS = "RESULT_OK"
private const val RESULT_BAD_CREDENTIALS = "RESULT_BAD_CREDENTIALS"
private const val RESULT_INVALID_TOKEN = "RESULT_INVALID_TOKEN"
private const val RESULT_ISSUES_NOT_ENABLED = "RESULT_ISSUES_NOT_ENABLED"
private const val RESULT_UNKNOWN = "RESULT_UNKNOWN"
@StringDef(
RESULT_SUCCESS,
RESULT_BAD_CREDENTIALS,
RESULT_INVALID_TOKEN,
RESULT_ISSUES_NOT_ENABLED,
RESULT_UNKNOWN
)
@Retention(AnnotationRetention.SOURCE)
private annotation class Result
open class BugReportActivity : AbsThemeActivity() {
@ -91,72 +55,20 @@ open class BugReportActivity : AbsThemeActivity() {
setSupportActionBar(binding.toolbar)
ToolbarContentTintHelper.colorBackButton(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
TintHelper.setTintAuto(binding.cardReport.optionUseAccount, accentColor, false)
binding.cardReport.optionUseAccount.setOnClickListener {
binding.cardReport.inputTitle.isEnabled = true
binding.cardReport.inputDescription.isEnabled = true
binding.cardReport.inputUsername.isEnabled = true
binding.cardReport.inputPassword.isEnabled = true
binding.cardReport.optionAnonymous.isChecked = false
binding.sendFab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
binding.sendFab.setImageResource(R.drawable.ic_send)
binding.sendFab.show()
}
})
}
TintHelper.setTintAuto(binding.cardReport.optionAnonymous, accentColor, false)
binding.cardReport.optionAnonymous.setOnClickListener {
binding.cardReport.inputTitle.isEnabled = false
binding.cardReport.inputDescription.isEnabled = false
binding.cardReport.inputUsername.isEnabled = false
binding.cardReport.inputPassword.isEnabled = false
binding.cardReport.optionUseAccount.isChecked = false
binding.sendFab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
override fun onHidden(fab: FloatingActionButton?) {
super.onHidden(fab)
binding.sendFab.setImageResource(R.drawable.ic_open_in_browser)
binding.sendFab.show()
}
})
}
binding.cardReport.inputPassword.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND) {
reportIssue()
return@setOnEditorActionListener true
}
false
}
binding.cardDeviceInfo.airTextDeviceInfo.setOnClickListener { copyDeviceInfoToClipBoard() }
TintHelper.setTintAuto(binding.sendFab, accentColor, true)
binding.sendFab.setOnClickListener { reportIssue() }
MaterialUtil.setTint(binding.cardReport.inputLayoutTitle, false)
MaterialUtil.setTint(binding.cardReport.inputLayoutDescription, false)
MaterialUtil.setTint(binding.cardReport.inputLayoutUsername, false)
MaterialUtil.setTint(binding.cardReport.inputLayoutPassword, false)
}
private fun reportIssue() {
if (binding.cardReport.optionUseAccount.isChecked) {
if (!validateInput()) return
val username = binding.cardReport.inputUsername.text.toString()
val password = binding.cardReport.inputPassword.text.toString()
sendBugReport(GithubLogin(username, password))
} else {
copyDeviceInfoToClipBoard()
copyDeviceInfoToClipBoard()
val i = Intent(Intent.ACTION_VIEW)
i.data = ISSUE_TRACKER_LINK.toUri()
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(i)
}
val i = Intent(Intent.ACTION_VIEW)
i.data = ISSUE_TRACKER_LINK.toUri()
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(i)
}
private fun copyDeviceInfoToClipBoard() {
@ -166,67 +78,6 @@ open class BugReportActivity : AbsThemeActivity() {
showToast(R.string.copied_device_info_to_clipboard)
}
private fun validateInput(): Boolean {
var hasErrors = false
if (binding.cardReport.optionUseAccount.isChecked) {
if (binding.cardReport.inputUsername.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutUsername, R.string.bug_report_no_username)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutUsername)
}
if (binding.cardReport.inputPassword.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutPassword, R.string.bug_report_no_password)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutPassword)
}
}
if (binding.cardReport.inputTitle.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutTitle, R.string.bug_report_no_title)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutTitle)
}
if (binding.cardReport.inputDescription.text.isNullOrEmpty()) {
setError(binding.cardReport.inputLayoutDescription, R.string.bug_report_no_description)
hasErrors = true
} else {
removeError(binding.cardReport.inputLayoutDescription)
}
return !hasErrors
}
private fun setError(editTextLayout: TextInputLayout, @StringRes errorRes: Int) {
editTextLayout.error = getString(errorRes)
}
private fun removeError(editTextLayout: TextInputLayout) {
editTextLayout.error = null
}
private fun sendBugReport(login: GithubLogin) {
if (!validateInput()) return
val bugTitle = binding.cardReport.inputTitle.text.toString()
val bugDescription = binding.cardReport.inputDescription.text.toString()
val extraInfo = ExtraInfo()
onSaveExtraInfo()
val report = Report(bugTitle, bugDescription, deviceInfo, extraInfo)
val target = GithubTarget("RetroMusicPlayer", "RetroMusicPlayer")
reportIssue(report, target, login)
}
private fun onSaveExtraInfo() {}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
@ -234,72 +85,6 @@ open class BugReportActivity : AbsThemeActivity() {
return super.onOptionsItemSelected(item)
}
private fun reportIssue(
report: Report,
target: GithubTarget,
login: GithubLogin
) {
val client: GitHubClient = if (login.shouldUseApiToken()) {
GitHubClient().setOAuth2Token(login.apiToken)
} else {
GitHubClient().setCredentials(login.username, login.password)
}
val issue = Issue().setTitle(report.title).setBody(report.getDescription())
lifecycleScope.launch(Dispatchers.IO) {
val result = try {
IssueService(client).createIssue(target.username, target.repository, issue)
RESULT_SUCCESS
} catch (e: RequestException) {
when (e.status) {
STATUS_BAD_CREDENTIALS -> {
if (login.shouldUseApiToken()) RESULT_INVALID_TOKEN else RESULT_BAD_CREDENTIALS
}
STATUS_ISSUES_NOT_ENABLED -> RESULT_ISSUES_NOT_ENABLED
else -> {
RESULT_UNKNOWN
throw e
}
}
} catch (e: IOException) {
e.printStackTrace()
RESULT_UNKNOWN
}
withContext(Dispatchers.Main) {
val activity = this@BugReportActivity
when (result) {
RESULT_SUCCESS -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_success)
.setPositiveButton(android.R.string.ok) { _, _ -> tryToFinishActivity() }
.show()
RESULT_BAD_CREDENTIALS -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_wrong_credentials)
.setPositiveButton(android.R.string.ok, null)
.show()
RESULT_INVALID_TOKEN -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_invalid_token)
.setPositiveButton(android.R.string.ok, null)
.show()
RESULT_ISSUES_NOT_ENABLED -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_issues_not_available)
.setPositiveButton(android.R.string.ok, null)
.show()
else -> MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bug_report_failed)
.setMessage(R.string.bug_report_failed_unknown)
.setPositiveButton(android.R.string.ok) { _, _ -> tryToFinishActivity() }
.setNegativeButton(android.R.string.cancel) { _, _ -> tryToFinishActivity() }
.show()
}
}
}
}
private fun tryToFinishActivity() {
if (!isFinishing) {
finish()
@ -307,10 +92,7 @@ open class BugReportActivity : AbsThemeActivity() {
}
companion object {
private const val STATUS_BAD_CREDENTIALS = 401
private const val STATUS_ISSUES_NOT_ENABLED = 410
private const val ISSUE_TRACKER_LINK =
"https://github.com/RetroMusicPlayer/RetroMusicPlayer"
"https://github.com/MuntashirAkon/Metro/issues/new"
}
}

View file

@ -1,22 +0,0 @@
package io.github.muntashirakon.music.activities.bugreport.model
import io.github.muntashirakon.music.activities.bugreport.model.github.ExtraInfo
class Report(
val title: String,
private val description: String,
private val deviceInfo: DeviceInfo?,
private val extraInfo: ExtraInfo
) {
fun getDescription(): String {
return """
$description
-
${deviceInfo?.toMarkdown()}
${extraInfo.toMarkdown()}
""".trimIndent()
}
}

View file

@ -1,61 +0,0 @@
package io.github.muntashirakon.music.activities.bugreport.model.github
class ExtraInfo {
private val extraInfo: MutableMap<String, String> = LinkedHashMap()
fun put(key: String, value: String) {
extraInfo[key] = value
}
fun put(key: String, value: Boolean) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Double) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Float) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Long) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Int) {
extraInfo[key] = value.toString()
}
fun put(key: String, value: Any) {
extraInfo[key] = value.toString()
}
fun remove(key: String) {
extraInfo.remove(key)
}
fun toMarkdown(): String {
if (extraInfo.isEmpty()) {
return ""
}
val output = StringBuilder()
output.append(
"""
Extra info:
---
<table>
""".trimIndent()
)
for (key in extraInfo.keys) {
output
.append("<tr><td>")
.append(key)
.append("</td><td>")
.append(extraInfo[key])
.append("</td></tr>\n")
}
output.append("</table>\n")
return output.toString()
}
}

View file

@ -1,25 +0,0 @@
package io.github.muntashirakon.music.activities.bugreport.model.github
import android.text.TextUtils
class GithubLogin {
val apiToken: String?
val password: String?
val username: String?
constructor(username: String?, password: String?) {
this.username = username
this.password = password
apiToken = null
}
constructor(apiToken: String?) {
username = null
password = null
this.apiToken = apiToken
}
fun shouldUseApiToken(): Boolean {
return TextUtils.isEmpty(username) || TextUtils.isEmpty(password)
}
}

View file

@ -1,3 +0,0 @@
package io.github.muntashirakon.music.activities.bugreport.model.github
class GithubTarget(val username: String, val repository: String)

View file

@ -22,7 +22,6 @@ import android.view.*
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
@ -59,13 +58,10 @@ import io.github.muntashirakon.music.interfaces.ICabCallback
import io.github.muntashirakon.music.interfaces.ICabHolder
import io.github.muntashirakon.music.model.Album
import io.github.muntashirakon.music.model.Artist
import io.github.muntashirakon.music.network.Result
import io.github.muntashirakon.music.network.model.LastFmAlbum
import io.github.muntashirakon.music.repository.RealRepository
import io.github.muntashirakon.music.util.MusicUtil
import io.github.muntashirakon.music.util.PreferenceUtil
import io.github.muntashirakon.music.util.RetroColorUtil
import io.github.muntashirakon.music.util.RetroUtil
import com.afollestad.materialcab.attached.AttachedCab
import com.afollestad.materialcab.attached.destroy
import com.afollestad.materialcab.attached.isActive
@ -162,14 +158,6 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det
)
}
binding.fragmentAlbumContent.aboutAlbumText.setOnClickListener {
if (binding.fragmentAlbumContent.aboutAlbumText.maxLines == 4) {
binding.fragmentAlbumContent.aboutAlbumText.maxLines = Integer.MAX_VALUE
} else {
binding.fragmentAlbumContent.aboutAlbumText.maxLines = 4
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
if (!handleBackPress()) {
remove()
@ -240,21 +228,6 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det
loadArtistImage(it)
}
}
detailsViewModel.getAlbumInfo(album).observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Loading -> {
println("Loading")
}
is Result.Error -> {
println("Error")
}
is Result.Success -> {
aboutAlbum(result.data)
}
}
}
}
private fun moreAlbums(albums: List<Album>) {
@ -274,41 +247,13 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det
binding.fragmentAlbumContent.moreRecyclerView.adapter = albumAdapter
}
private fun aboutAlbum(lastFmAlbum: LastFmAlbum) {
if (lastFmAlbum.album != null) {
if (lastFmAlbum.album.wiki != null) {
binding.fragmentAlbumContent.aboutAlbumText.show()
binding.fragmentAlbumContent.aboutAlbumTitle.show()
binding.fragmentAlbumContent.aboutAlbumTitle.text =
String.format(getString(R.string.about_album_label), lastFmAlbum.album.name)
binding.fragmentAlbumContent.aboutAlbumText.text =
lastFmAlbum.album.wiki.content.parseAsHtml()
}
if (lastFmAlbum.album.listeners.isNotEmpty()) {
binding.fragmentAlbumContent.listeners.show()
binding.fragmentAlbumContent.listenersLabel.show()
binding.fragmentAlbumContent.scrobbles.show()
binding.fragmentAlbumContent.scrobblesLabel.show()
binding.fragmentAlbumContent.listeners.text =
RetroUtil.formatValue(lastFmAlbum.album.listeners.toFloat())
binding.fragmentAlbumContent.scrobbles.text =
RetroUtil.formatValue(lastFmAlbum.album.playcount.toFloat())
}
}
}
private fun loadArtistImage(artist: Artist) {
detailsViewModel.getMoreAlbums(artist).observe(viewLifecycleOwner) {
moreAlbums(it)
}
GlideApp.with(requireContext())
//.forceDownload(PreferenceUtil.isAllowedToDownloadMetadata())
.load(
RetroGlideExtension.getArtistModel(
artist,
PreferenceUtil.isAllowedToDownloadMetadata(requireContext())
)
RetroGlideExtension.getArtistModel(artist)
)
.artistImageOptions(artist)
.dontAnimate()

View file

@ -18,8 +18,6 @@ import androidx.lifecycle.*
import io.github.muntashirakon.music.interfaces.IMusicServiceEventListener
import io.github.muntashirakon.music.model.Album
import io.github.muntashirakon.music.model.Artist
import io.github.muntashirakon.music.network.Result
import io.github.muntashirakon.music.network.model.LastFmAlbum
import io.github.muntashirakon.music.repository.RealRepository
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
@ -52,11 +50,6 @@ class AlbumDetailsViewModel(
emit(artist)
}
fun getAlbumInfo(album: Album): LiveData<Result<LastFmAlbum>> = liveData(IO) {
emit(Result.Loading)
emit(repository.albumInfo(album.artistName, album.title))
}
fun getMoreAlbums(artist: Artist): LiveData<List<Album>> = liveData(IO) {
artist.albums.filter { item -> item.id != albumId }.let { albums ->
if (albums.isNotEmpty()) emit(albums)

View file

@ -13,9 +13,7 @@ import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.doOnPreDraw
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
@ -39,8 +37,6 @@ import io.github.muntashirakon.music.interfaces.IAlbumClickListener
import io.github.muntashirakon.music.interfaces.ICabCallback
import io.github.muntashirakon.music.interfaces.ICabHolder
import io.github.muntashirakon.music.model.Artist
import io.github.muntashirakon.music.network.Result
import io.github.muntashirakon.music.network.model.LastFmArtist
import io.github.muntashirakon.music.repository.RealRepository
import io.github.muntashirakon.music.util.*
import com.afollestad.materialcab.attached.AttachedCab
@ -67,8 +63,6 @@ abstract class AbsArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragm
private lateinit var songAdapter: SimpleSongAdapter
private lateinit var albumAdapter: HorizontalAlbumAdapter
private var forceDownload: Boolean = false
private var lang: String? = null
private var biography: Spanned? = null
private val savedSongSortOrder: String
get() = PreferenceUtil.artistDetailSongSortOrder
@ -105,14 +99,6 @@ abstract class AbsArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragm
setOnClickListener { MusicPlayerRemote.openAndShuffleQueue(artist.songs, true) }
}
binding.fragmentArtistContent.biographyText.setOnClickListener {
if (binding.fragmentArtistContent.biographyText.maxLines == 4) {
binding.fragmentArtistContent.biographyText.maxLines = Integer.MAX_VALUE
} else {
binding.fragmentArtistContent.biographyText.maxLines = 4
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
if (!handleBackPress()) {
remove()
@ -146,9 +132,6 @@ abstract class AbsArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragm
}
this.artist = artist
loadArtistImage(artist)
if (PreferenceUtil.isAllowedToDownloadMetadata(requireContext())) {
loadBiography(artist.name)
}
binding.artistTitle.text = artist.name
binding.text.text = String.format(
"%s • %s",
@ -171,51 +154,6 @@ abstract class AbsArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragm
albumAdapter.swapDataSet(artist.albums)
}
private fun loadBiography(
name: String,
lang: String? = Locale.getDefault().language,
) {
biography = null
this.lang = lang
detailsViewModel.getArtistInfo(name, lang, null)
.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Loading -> println("Loading")
is Result.Error -> println("Error")
is Result.Success -> artistInfo(result.data)
}
}
}
private fun artistInfo(lastFmArtist: LastFmArtist?) {
if (lastFmArtist != null && lastFmArtist.artist != null && lastFmArtist.artist.bio != null) {
val bioContent = lastFmArtist.artist.bio.content
if (bioContent != null && bioContent.trim { it <= ' ' }.isNotEmpty()) {
binding.fragmentArtistContent.run {
biographyText.isVisible = true
biographyTitle.isVisible = true
biography = bioContent.parseAsHtml()
biographyText.text = biography
if (lastFmArtist.artist.stats.listeners.isNotEmpty()) {
listeners.show()
listenersLabel.show()
scrobbles.show()
scrobblesLabel.show()
listeners.text =
RetroUtil.formatValue(lastFmArtist.artist.stats.listeners.toFloat())
scrobbles.text =
RetroUtil.formatValue(lastFmArtist.artist.stats.playcount.toFloat())
}
}
}
}
// If the "lang" parameter is set and no biography is given, retry with default language
if (biography == null && lang != null) {
loadBiography(artist.name, null)
}
}
private fun loadArtistImage(artist: Artist) {
GlideApp.with(requireContext()).asBitmapPalette().artistImageOptions(artist)
.load(RetroGlideExtension.getArtistModel(artist))

View file

@ -17,8 +17,6 @@ package io.github.muntashirakon.music.fragments.artists
import androidx.lifecycle.*
import io.github.muntashirakon.music.interfaces.IMusicServiceEventListener
import io.github.muntashirakon.music.model.Artist
import io.github.muntashirakon.music.network.Result
import io.github.muntashirakon.music.network.model.LastFmArtist
import io.github.muntashirakon.music.repository.RealRepository
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
@ -44,16 +42,6 @@ class ArtistDetailsViewModel(
fun getArtist(): LiveData<Artist> = artistDetails
fun getArtistInfo(
name: String,
lang: String?,
cache: String?
): LiveData<Result<LastFmArtist>> = liveData(IO) {
emit(Result.Loading)
val info = realRepository.artistInfo(name, lang, cache)
emit(info)
}
override fun onMediaStoreChanged() {
fetchArtist()
}

View file

@ -15,9 +15,6 @@
package io.github.muntashirakon.music.fragments.settings
import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import io.github.muntashirakon.music.AUTO_DOWNLOAD_IMAGES_POLICY
import io.github.muntashirakon.music.R
/**
@ -25,22 +22,9 @@ import io.github.muntashirakon.music.R
*/
class ImageSettingFragment : AbsSettingsFragment() {
override fun invalidateSettings() {
val autoDownloadImagesPolicy: Preference = findPreference(AUTO_DOWNLOAD_IMAGES_POLICY)!!
setSummary(autoDownloadImagesPolicy)
autoDownloadImagesPolicy.setOnPreferenceChangeListener { _, o ->
setSummary(autoDownloadImagesPolicy, o)
true
}
}
override fun invalidateSettings() {}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_images)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val preference: Preference? = findPreference(AUTO_DOWNLOAD_IMAGES_POLICY)
preference?.let { setSummary(it) }
}
}

View file

@ -1,36 +1,19 @@
package io.github.muntashirakon.music.glide.artistimage
import android.content.Context
import io.github.muntashirakon.music.model.Data
import io.github.muntashirakon.music.model.DeezerResponse
import io.github.muntashirakon.music.network.DeezerService
import io.github.muntashirakon.music.util.MusicUtil
import io.github.muntashirakon.music.util.PreferenceUtil
import com.bumptech.glide.Priority
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.GlideUrl
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import io.github.muntashirakon.music.util.MusicUtil
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
class ArtistImageFetcher(
private val context: Context,
private val deezerService: DeezerService,
val model: ArtistImage,
private val okhttp: OkHttpClient
) : DataFetcher<InputStream> {
private var streamFetcher: OkHttpStreamFetcher? = null
private var response: Call<DeezerResponse>? = null
private var isCancelled: Boolean = false
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
@ -40,52 +23,8 @@ class ArtistImageFetcher(
}
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
try {
if (!MusicUtil.isArtistNameUnknown(model.artist.name) &&
PreferenceUtil.isAllowedToDownloadMetadata(context)
) {
val artists = model.artist.name.split(",", "&")
response = deezerService.getArtistImage(artists[0])
response?.enqueue(object : Callback<DeezerResponse> {
override fun onResponse(
call: Call<DeezerResponse>,
response: Response<DeezerResponse>
) {
if (!response.isSuccessful) {
throw IOException("Request failed with code: " + response.code())
}
if (isCancelled) {
callback.onDataReady(null)
return
}
try {
val deezerResponse = response.body()
val imageUrl =
deezerResponse?.data?.get(0)?.let { getHighestQuality(it) }
// Fragile way to detect a place holder image returned from Deezer:
// ex: "https://e-cdns-images.dzcdn.net/images/artist//250x250-000000-80-0-0.jpg"
// the double slash implies no artist identified
val placeHolder = imageUrl?.contains("/images/artist//") ?: false
if (!placeHolder) {
streamFetcher = OkHttpStreamFetcher(okhttp, GlideUrl(imageUrl))
streamFetcher?.loadData(priority, callback)
} else {
callback.onDataReady(getFallbackAlbumImage())
}
} catch (e: Exception) {
callback.onDataReady(getFallbackAlbumImage())
}
}
override fun onFailure(call: Call<DeezerResponse>, t: Throwable) {
callback.onDataReady(getFallbackAlbumImage())
}
})
} else callback.onDataReady(null)
} catch (e: Exception) {
callback.onLoadFailed(e)
if (!MusicUtil.isArtistNameUnknown(model.artist.name)) {
callback.onDataReady(getFallbackAlbumImage())
}
}
@ -106,24 +45,9 @@ class ArtistImageFetcher(
}
}
private fun getHighestQuality(imageUrl: Data): String {
return when {
imageUrl.pictureXl.isNotEmpty() -> imageUrl.pictureXl
imageUrl.pictureBig.isNotEmpty() -> imageUrl.pictureBig
imageUrl.pictureMedium.isNotEmpty() -> imageUrl.pictureMedium
imageUrl.pictureSmall.isNotEmpty() -> imageUrl.pictureSmall
imageUrl.picture.isNotEmpty() -> imageUrl.picture
else -> ""
}
}
override fun cleanup() {
streamFetcher?.cleanup()
}
override fun cancel() {
isCancelled = true
response?.cancel()
streamFetcher?.cancel()
}
}

View file

@ -15,23 +15,16 @@
package io.github.muntashirakon.music.glide.artistimage
import android.content.Context
import io.github.muntashirakon.music.network.DeezerService
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoader.LoadData
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import java.io.InputStream
import java.util.concurrent.TimeUnit
class ArtistImageLoader(
val context: Context,
private val deezerService: DeezerService,
private val okhttp: OkHttpClient
) : ModelLoader<ArtistImage, InputStream> {
override fun buildLoadData(
@ -42,7 +35,7 @@ class ArtistImageLoader(
): LoadData<InputStream> {
return LoadData(
ObjectKey(model.artist.name),
ArtistImageFetcher(context, deezerService, model, okhttp)
ArtistImageFetcher(context, model)
)
}
@ -55,44 +48,9 @@ class Factory(
val context: Context
) : ModelLoaderFactory<ArtistImage, InputStream> {
private var deezerService: DeezerService
private var okHttp: OkHttpClient
init {
okHttp =
OkHttpClient.Builder()
.connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.build()
deezerService = DeezerService.invoke(
DeezerService.createDefaultOkHttpClient(context)
.connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(createLogInterceptor())
.build()
)
}
private fun createLogInterceptor(): Interceptor {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
return interceptor
}
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ArtistImage, InputStream> {
return ArtistImageLoader(
context,
deezerService,
okHttp
)
return ArtistImageLoader(context)
}
override fun teardown() {}
companion object {
// we need these very low values to make sure our artist image loading calls doesn't block the image loading queue
private const val TIMEOUT: Long = 500
}
}

View file

@ -1,27 +0,0 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.model
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
class Contributor(
@SerializedName("name") val name: String = "",
@SerializedName("summary") val summary: String = "",
@SerializedName("link") val link: String = "",
@SerializedName("image") val image: String = ""
) : Parcelable

View file

@ -1,41 +0,0 @@
package io.github.muntashirakon.music.model
import com.google.gson.annotations.SerializedName
data class Data(
@SerializedName("id")
val id: String,
@SerializedName("link")
val link: String,
@SerializedName("name")
val name: String,
@SerializedName("nb_album")
val nbAlbum: Int,
@SerializedName("nb_fan")
val nbFan: Int,
@SerializedName("picture")
val picture: String,
@SerializedName("picture_big")
val pictureBig: String,
@SerializedName("picture_medium")
val pictureMedium: String,
@SerializedName("picture_small")
val pictureSmall: String,
@SerializedName("picture_xl")
val pictureXl: String,
@SerializedName("radio")
val radio: Boolean,
@SerializedName("tracklist")
val tracklist: String,
@SerializedName("type")
val type: String
)
data class DeezerResponse(
@SerializedName("data")
val data: List<Data>,
@SerializedName("next")
val next: String,
@SerializedName("total")
val total: Int
)

View file

@ -1,69 +0,0 @@
package io.github.muntashirakon.music.network
import android.content.Context
import io.github.muntashirakon.music.model.DeezerResponse
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import retrofit2.http.GET
import retrofit2.http.Query
import java.io.File
import java.util.*
private const val BASE_QUERY_ARTIST = "search/artist"
private const val BASE_URL = "https://api.deezer.com/"
interface DeezerService {
@GET("$BASE_QUERY_ARTIST&limit=1")
fun getArtistImage(
@Query("q") artistName: String
): Call<DeezerResponse>
companion object {
operator fun invoke(
client: okhttp3.Call.Factory
): DeezerService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.callFactory(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create()
}
fun createDefaultOkHttpClient(
context: Context
): OkHttpClient.Builder = OkHttpClient.Builder()
.cache(createDefaultCache(context))
.addInterceptor(createCacheControlInterceptor())
private fun createDefaultCache(
context: Context
): Cache? {
val cacheDir = File(context.applicationContext.cacheDir.absolutePath, "/okhttp-deezer/")
if (cacheDir.mkdir() or cacheDir.isDirectory) {
return Cache(cacheDir, 1024 * 1024 * 10)
}
return null
}
private fun createCacheControlInterceptor(): Interceptor {
return Interceptor { chain ->
val modifiedRequest = chain.request().newBuilder()
.addHeader(
"Cache-Control",
String.format(
Locale.getDefault(),
"max-age=31536000, max-stale=31536000"
)
).build()
chain.proceed(modifiedRequest)
}
}
}
}

View file

@ -1,45 +0,0 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.network
import io.github.muntashirakon.music.network.model.LastFmAlbum
import io.github.muntashirakon.music.network.model.LastFmArtist
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
/**
* Created by hemanths on 2019-11-26.
*/
interface LastFMService {
companion object {
private const val API_KEY = "c679c8d3efa84613dc7dcb2e8d42da4c"
const val BASE_QUERY_PARAMETERS = "?format=json&autocorrect=1&api_key=$API_KEY"
}
@GET("$BASE_QUERY_PARAMETERS&method=artist.getinfo")
suspend fun artistInfo(
@Query("artist") artistName: String,
@Query("lang") language: String?,
@Header("Cache-Control") cacheControl: String?
): LastFmArtist
@GET("$BASE_QUERY_PARAMETERS&method=album.getinfo")
suspend fun albumInfo(
@Query("artist") artistName: String,
@Query("album") albumName: String
): LastFmAlbum
}

View file

@ -1,12 +0,0 @@
package io.github.muntashirakon.music.network
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Query
interface LyricsRestService {
@Headers("Cache-Control: public")
@GET("/lyrics")
suspend fun getLyrics(@Query("artist") artist: String, @Query("title") title: String): String
}

View file

@ -1,25 +0,0 @@
/*
* Copyright (C) 2020 Fatih Giris. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.muntashirakon.music.network
/**
* Generic class that holds the network state
*/
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
object Loading : Result<Nothing>()
data class Error(val error: Exception) : Result<Nothing>()
}

View file

@ -1,87 +0,0 @@
package io.github.muntashirakon.music.network
import android.content.Context
import io.github.muntashirakon.music.App
import io.github.muntashirakon.music.BuildConfig
import io.github.muntashirakon.music.network.conversion.LyricsConverterFactory
import com.google.gson.GsonBuilder
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit
fun provideDefaultCache(): Cache? {
val cacheDir = File(App.getContext().cacheDir.absolutePath, "/okhttp-lastfm/")
if (cacheDir.mkdirs() || cacheDir.isDirectory) {
return Cache(cacheDir, 1024 * 1024 * 10)
}
return null
}
fun logInterceptor(): Interceptor {
val loggingInterceptor = HttpLoggingInterceptor()
if (BuildConfig.DEBUG) {
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
} else {
// disable retrofit log on release
loggingInterceptor.level = HttpLoggingInterceptor.Level.NONE
}
return loggingInterceptor
}
fun headerInterceptor(context: Context): Interceptor {
return Interceptor {
val original = it.request()
val request = original.newBuilder()
.header("User-Agent", context.packageName)
.addHeader("Content-Type", "application/json; charset=utf-8")
.method(original.method, original.body)
.build()
it.proceed(request)
}
}
fun provideOkHttp(context: Context, cache: Cache): OkHttpClient {
return OkHttpClient.Builder()
.addNetworkInterceptor(logInterceptor())
//.addInterceptor(headerInterceptor(context))
.connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(1, TimeUnit.SECONDS)
.cache(cache)
.build()
}
fun provideLastFmRetrofit(client: OkHttpClient): Retrofit {
val gson = GsonBuilder()
.setLenient()
.create()
return Retrofit.Builder()
.baseUrl("https://ws.audioscrobbler.com/2.0/")
.addConverterFactory(GsonConverterFactory.create(gson))
.callFactory { request -> client.newCall(request) }
.build()
}
fun provideLastFmRest(retrofit: Retrofit): LastFMService {
return retrofit.create(LastFMService::class.java)
}
fun provideDeezerRest(retrofit: Retrofit): DeezerService {
val newBuilder = retrofit.newBuilder()
.baseUrl("https://api.deezer.com/")
.build()
return newBuilder.create(DeezerService::class.java)
}
fun provideLyrics(retrofit: Retrofit): LyricsRestService {
val newBuilder = retrofit.newBuilder()
.baseUrl("https://makeitpersonal.co")
.addConverterFactory(LyricsConverterFactory())
.build()
return newBuilder.create(LyricsRestService::class.java)
}

View file

@ -1,52 +0,0 @@
/*
* Copyright (c) 2019 Naman Dwivedi.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
*/
package io.github.muntashirakon.music.network.conversion
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
class LyricsConverterFactory : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
return if (String::class.java == type) {
Converter<ResponseBody, String> { value -> value.string() }
} else null
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<Annotation>,
methodAnnotations: Array<Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody>? {
return if (String::class.java == type) {
Converter<String, RequestBody> { value -> value.toRequestBody(MEDIA_TYPE) }
} else null
}
companion object {
private val MEDIA_TYPE = "text/plain".toMediaTypeOrNull()
}
}

View file

@ -1,161 +0,0 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.network.model;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
public class LastFmAlbum {
@Expose private Album album;
public Album getAlbum() {
return album;
}
public void setAlbum(Album album) {
this.album = album;
}
public static class Album {
@Expose public String listeners;
@Expose public String playcount;
@Expose private List<Image> image = new ArrayList<>();
@Expose private String name;
@Expose private Tags tags;
@Expose private Wiki wiki;
public List<Image> getImage() {
return image;
}
public void setImage(List<Image> image) {
this.image = image;
}
public String getListeners() {
return listeners;
}
public void setListeners(final String listeners) {
this.listeners = listeners;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public String getPlaycount() {
return playcount;
}
public void setPlaycount(final String playcount) {
this.playcount = playcount;
}
public Tags getTags() {
return tags;
}
public Wiki getWiki() {
return wiki;
}
public void setWiki(Wiki wiki) {
this.wiki = wiki;
}
public static class Image {
@SerializedName("#text")
@Expose
private String Text;
@Expose private String size;
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public String getText() {
return Text;
}
public void setText(String Text) {
this.Text = Text;
}
}
public class Tags {
@Expose
private final List<Tag> tag = null;
public List<Tag> getTag() {
return tag;
}
}
public class Tag {
@Expose private String name;
@Expose private String url;
public String getName() {
return name;
}
public String getUrl() {
return url;
}
}
public class Wiki {
@Expose private String content;
@Expose private String published;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getPublished() {
return published;
}
public void setPublished(final String published) {
this.published = published;
}
}
}
}

View file

@ -1,118 +0,0 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.network.model;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
public class LastFmArtist {
@Expose private Artist artist;
public Artist getArtist() {
return artist;
}
public void setArtist(Artist artist) {
this.artist = artist;
}
public static class Artist {
@Expose public Stats stats;
@Expose private Bio bio;
@Expose private List<Image> image = new ArrayList<>();
public Bio getBio() {
return bio;
}
public void setBio(Bio bio) {
this.bio = bio;
}
public List<Image> getImage() {
return image;
}
public void setImage(List<Image> image) {
this.image = image;
}
public static class Image {
@SerializedName("#text")
@Expose
private String Text;
@Expose private String size;
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public String getText() {
return Text;
}
public void setText(String Text) {
this.Text = Text;
}
}
public static class Stats {
@Expose public String listeners;
@Expose public String playcount;
public String getListeners() {
return listeners;
}
public void setListeners(final String listeners) {
this.listeners = listeners;
}
public String getPlaycount() {
return playcount;
}
public void setPlaycount(final String playcount) {
this.playcount = playcount;
}
}
public class Bio {
@Expose private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
}
}

View file

@ -1,174 +0,0 @@
/*
* Copyright (c) 2019 Hemanth Savarala.
*
* Licensed under the GNU General Public License v3
*
* This is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*/
package io.github.muntashirakon.music.network.model;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/** Created by hemanths on 15/06/17. */
public class LastFmTrack {
@Expose private Track track;
public Track getTrack() {
return track;
}
public void setTrack(Track track) {
this.track = track;
}
public static class Track {
@SerializedName("name")
@Expose
private String name;
@Expose private Album album;
@Expose private Wiki wiki;
@Expose private Toptags toptags;
@Expose private Artist artist;
public Album getAlbum() {
return album;
}
public Wiki getWiki() {
return wiki;
}
public String getName() {
return name;
}
public Toptags getToptags() {
return toptags;
}
public static class Artist {
@Expose private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static class Wiki {
@Expose private String published;
public String getPublished() {
return published;
}
public void setPublished(String published) {
this.published = published;
}
}
public static class Toptags {
@Expose
private final List<Tag> tag = null;
public List<Tag> getTag() {
return tag;
}
public static class Tag {
@Expose private String name;
public String getName() {
return name;
}
}
}
public static class Album {
@Expose private String artist;
@Expose private List<Image> image = null;
@Expose private String title;
@SerializedName("@attr")
@Expose
private Attr attr;
public Attr getAttr() {
return attr;
}
public void setAttr(Attr attr) {
this.attr = attr;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public List<Image> getImage() {
return image;
}
public void setImage(List<Image> image) {
this.image = image;
}
public static class Attr {
@Expose private String position;
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
}
public class Image {
@SerializedName("#text")
@Expose
private String text;
@Expose private String size;
public String getSize() {
return size;
}
public String getText() {
return text;
}
}
}
}
}

View file

@ -22,24 +22,9 @@ import io.github.muntashirakon.music.db.*
import io.github.muntashirakon.music.fragments.search.Filter
import io.github.muntashirakon.music.model.*
import io.github.muntashirakon.music.model.smartplaylist.NotPlayedPlaylist
import io.github.muntashirakon.music.network.LastFMService
import io.github.muntashirakon.music.network.Result
import io.github.muntashirakon.music.network.Result.*
import io.github.muntashirakon.music.network.model.LastFmAlbum
import io.github.muntashirakon.music.network.model.LastFmArtist
import io.github.muntashirakon.music.util.PreferenceUtil
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
interface Repository {
fun songsFlow(): Flow<Result<List<Song>>>
fun albumsFlow(): Flow<Result<List<Album>>>
fun artistsFlow(): Flow<Result<List<Artist>>>
fun playlistsFlow(): Flow<Result<List<Playlist>>>
fun genresFlow(): Flow<Result<List<Genre>>>
fun historySong(): List<HistoryEntity>
fun favorites(): LiveData<List<SongEntity>>
fun observableHistorySongs(): LiveData<List<Song>>
@ -55,8 +40,6 @@ interface Repository {
suspend fun search(query: String?, filter: Filter): MutableList<Any>
suspend fun getPlaylistSongs(playlist: Playlist): List<Song>
suspend fun getGenre(genreId: Long): List<Song>
suspend fun artistInfo(name: String, lang: String?, cache: String?): Result<LastFmArtist>
suspend fun albumInfo(artist: String, album: String): Result<LastFmAlbum>
suspend fun artistById(artistId: Long): Artist
suspend fun albumArtistByName(name: String): Artist
suspend fun recentArtists(): List<Artist>
@ -74,8 +57,6 @@ interface Repository {
suspend fun playlists(): Home
suspend fun homeSections(): List<Home>
@ExperimentalCoroutinesApi
suspend fun homeSectionsFlow(): Flow<Result<List<Home>>>
suspend fun playlist(playlistId: Long): Playlist
suspend fun fetchPlaylistWithSongs(): List<PlaylistWithSongs>
suspend fun playlistSongs(playlistWithSongs: PlaylistWithSongs): List<Song>
@ -115,7 +96,6 @@ interface Repository {
class RealRepository(
private val context: Context,
private val lastFMService: LastFMService,
private val songRepository: SongRepository,
private val albumRepository: AlbumRepository,
private val artistRepository: ArtistRepository,
@ -183,59 +163,6 @@ class RealRepository(
override suspend fun getGenre(genreId: Long): List<Song> = genreRepository.songs(genreId)
override suspend fun artistInfo(
name: String,
lang: String?,
cache: String?
): Result<LastFmArtist> {
return try {
Success(lastFMService.artistInfo(name, lang, cache))
} catch (e: Exception) {
println(e)
Error(e)
}
}
override suspend fun albumInfo(
artist: String,
album: String
): Result<LastFmAlbum> {
return try {
val lastFmAlbum = lastFMService.albumInfo(artist, album)
Success(lastFmAlbum)
} catch (e: Exception) {
println(e)
Error(e)
}
}
@ExperimentalCoroutinesApi
override suspend fun homeSectionsFlow(): Flow<Result<List<Home>>> {
val homes = MutableStateFlow<Result<List<Home>>>(value = Loading)
val homeSections = mutableListOf<Home>()
val sections = listOf(
topArtistsHome(),
topAlbumsHome(),
recentArtistsHome(),
recentAlbumsHome(),
suggestionsHome(),
favoritePlaylistHome(),
genresHome()
)
for (section in sections) {
if (section.arrayList.isNotEmpty()) {
println("${section.homeSection} -> ${section.arrayList.size}")
homeSections.add(section)
}
}
if (homeSections.isEmpty()) {
homes.value = Error(Exception(Throwable("No items")))
} else {
homes.value = Success(homeSections)
}
return homes
}
override suspend fun homeSections(): List<Home> {
val homeSections = mutableListOf<Home>()
val sections: List<Home> = listOf(
@ -420,53 +347,4 @@ class RealRepository(
return Home(songs, FAVOURITES, R.string.favorites)
}
override fun songsFlow(): Flow<Result<List<Song>>> = flow {
emit(Loading)
val data = songRepository.songs()
if (data.isEmpty()) {
emit(Error(Exception(Throwable("No items"))))
} else {
emit(Success(data))
}
}
override fun albumsFlow(): Flow<Result<List<Album>>> = flow {
emit(Loading)
val data = albumRepository.albums()
if (data.isEmpty()) {
emit(Error(Exception(Throwable("No items"))))
} else {
emit(Success(data))
}
}
override fun artistsFlow(): Flow<Result<List<Artist>>> = flow {
emit(Loading)
val data = artistRepository.artists()
if (data.isEmpty()) {
emit(Error(Exception(Throwable("No items"))))
} else {
emit(Success(data))
}
}
override fun playlistsFlow(): Flow<Result<List<Playlist>>> = flow {
emit(Loading)
val data = playlistRepository.playlists()
if (data.isEmpty()) {
emit(Error(Exception(Throwable("No items"))))
} else {
emit(Success(data))
}
}
override fun genresFlow(): Flow<Result<List<Genre>>> = flow {
emit(Loading)
val data = genreRepository.genres()
if (data.isEmpty()) {
emit(Error(Exception(Throwable("No items"))))
} else {
emit(Success(data))
}
}
}

View file

@ -1,11 +1,7 @@
package io.github.muntashirakon.music.util
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.core.content.res.use
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
@ -115,12 +111,6 @@ object PreferenceUtil {
putString(SAF_SDCARD_URI, value)
}
val autoDownloadImagesPolicy
get() = sharedPreferences.getStringOrDefault(
AUTO_DOWNLOAD_IMAGES_POLICY,
"only_wifi"
)
var albumArtistsOnly
get() = sharedPreferences.getBoolean(
ALBUM_ARTISTS_ONLY,
@ -336,26 +326,6 @@ object PreferenceUtil {
val isLockScreen get() = sharedPreferences.getBoolean(LOCK_SCREEN, false)
fun isAllowedToDownloadMetadata(context: Context): Boolean {
return when (autoDownloadImagesPolicy) {
"always" -> true
"only_wifi" -> {
val connectivityManager = context.getSystemService<ConnectivityManager>()
if (VersionUtils.hasMarshmallow()) {
val network = connectivityManager?.activeNetwork
val capabilities = connectivityManager?.getNetworkCapabilities(network)
capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
} else {
val netInfo = connectivityManager?.activeNetworkInfo
netInfo != null && netInfo.type == ConnectivityManager.TYPE_WIFI && netInfo.isConnectedOrConnecting
}
}
"never" -> false
else -> false
}
}
var lyricsOption
get() = sharedPreferences.getInt(LYRICS_OPTIONS, 1)
set(value) = sharedPreferences.edit {

View file

@ -33,13 +33,6 @@
android:clipToPadding="false"
android:orientation="vertical">
<include
android:id="@+id/card_report"
layout="@layout/bug_report_card_report"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<include
android:id="@+id/card_device_info"
layout="@layout/bug_report_card_device_info"
@ -60,5 +53,5 @@
style="@style/Fab"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:srcCompat="@drawable/ic_send" />
app:srcCompat="@drawable/ic_open_in_browser" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,185 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="@dimen/md_listitem_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/optionUseAccount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:gravity="start|center_vertical"
android:minHeight="@dimen/md_listitem_height" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="8dp"
android:text="@string/bug_report_use_account"
android:textAppearance="@style/TextViewSubtitle1" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="8dp"
android:text="@string/your_account_data_is_only_used_for_authentication"
android:textAppearance="@style/TextViewCaption" />
</LinearLayout>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="72dp"
android:paddingEnd="16dp">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/bug_report_issue"
android:textAppearance="@style/TextViewOverline"
android:textColor="?android:textColorSecondary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/inputLayoutTitle"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title"
android:inputType="textCapSentences"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/inputLayoutDescription"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:hint="@string/description"
android:inputType="textMultiLine"
android:minLines="3" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/login"
android:textAppearance="@style/TextViewOverline"
android:textColor="?android:textColorSecondary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/inputLayoutUsername"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username"
android:inputType="textNoSuggestions"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/inputLayoutPassword"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:imeOptions="actionSend"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="@dimen/md_listitem_height"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/optionAnonymous"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:minHeight="@dimen/md_listitem_height" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="8dp"
android:text="@string/bug_report_manual"
android:textAppearance="@style/TextViewSubtitle1" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="8dp"
android:text="@string/you_will_be_forwarded_to_the_issue_tracker_website"
android:textAppearance="@style/TextViewCaption" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View file

@ -97,106 +97,10 @@
tools:listitem="@layout/item_album_card"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/aboutAlbumTitle"
style="@style/SubTitleTextAppearance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/songs"
android:textAppearance="@style/TextViewOverline"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/moreRecyclerView"
tools:text="About Album"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/aboutAlbumText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lineSpacingExtra="5dp"
android:maxLines="4"
android:padding="16dp"
android:textSize="14sp"
android:textAppearance="@style/TextViewBody1"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/aboutAlbumTitle"
tools:text="@tools:sample/lorem/random"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/listenersLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/listeners_label"
android:textAppearance="@style/TextViewSubtitle1"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/aboutAlbumText"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/scrobblesLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
android:text="@string/scrobbles_label"
android:textAppearance="@style/TextViewSubtitle1"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/listenersLabel"
app:layout_constraintTop_toBottomOf="@id/aboutAlbumText"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/listeners"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="@style/TextViewBody1"
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="@id/listenersLabel"
app:layout_constraintStart_toStartOf="@id/listenersLabel"
app:layout_constraintTop_toBottomOf="@id/listenersLabel"
tools:text="100000"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/scrobbles"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="@style/TextViewBody1"
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="@id/scrobblesLabel"
app:layout_constraintStart_toStartOf="@id/scrobblesLabel"
app:layout_constraintTop_toBottomOf="@id/scrobblesLabel"
tools:text="100000"
tools:visibility="visible" />
<Space
android:layout_width="match_parent"
android:layout_height="72dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/listeners" />
app:layout_constraintTop_toBottomOf="@id/moreRecyclerView" />
</io.github.muntashirakon.music.views.insets.InsetsConstraintLayout>

View file

@ -107,106 +107,9 @@
app:layout_constraintTop_toBottomOf="@id/songTitle"
tools:listitem="@layout/item_song" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/biographyTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/biography"
android:textAppearance="@style/TextViewOverline"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recyclerView"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/biographyText"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lineSpacingExtra="5dp"
android:maxLines="4"
android:padding="16dp"
android:visibility="gone"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/biographyTitle"
tools:text="@string/bug_report_summary"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/listenersLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/listeners_label"
android:textAppearance="@style/TextViewSubtitle1"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/biographyText"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/scrobblesLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
android:text="@string/scrobbles_label"
android:textAppearance="@style/TextViewSubtitle1"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/listenersLabel"
app:layout_constraintTop_toBottomOf="@id/biographyText"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/listeners"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="@style/TextViewBody2"
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="@id/listenersLabel"
app:layout_constraintStart_toStartOf="@id/listenersLabel"
app:layout_constraintTop_toBottomOf="@id/listenersLabel"
tools:text="100000"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/scrobbles"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="@style/TextViewBody2"
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="@id/scrobblesLabel"
app:layout_constraintStart_toStartOf="@id/scrobblesLabel"
app:layout_constraintTop_toBottomOf="@id/scrobblesLabel"
tools:text="100000"
tools:visibility="visible" />
<Space
android:layout_width="match_parent"
android:layout_height="72dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/listeners" />
app:layout_constraintTop_toBottomOf="@id/recyclerView" />
</io.github.muntashirakon.music.views.insets.InsetsConstraintLayout>

View file

@ -10,11 +10,4 @@
android:title="@string/pref_title_ignore_media_store_artwork"
app:icon="@drawable/ic_image" />
<code.name.monkey.appthemehelper.common.prefs.supportv7.ATEListPreference
android:defaultValue="always"
android:entries="@array/pref_auto_download_images_titles"
android:entryValues="@array/pref_auto_download_images_values"
android:key="auto_download_images_policy"
android:layout="@layout/list_item_view"
android:title="@string/pref_title_auto_download_artist_images" />
</androidx.preference.PreferenceScreen>