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 "androidx.core:core-splashscreen:1.0.0-beta02"
implementation "com.google.android.material:material:$mdc_version" implementation "com.google.android.material:material:$mdc_version"
implementation 'com.google.code.gson:gson:2.9.0'
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'
def material_dialog_version = "3.3.0" def material_dialog_version = "3.3.0"
implementation "com.afollestad.material-dialogs:core:$material_dialog_version" implementation "com.afollestad.material-dialogs:core:$material_dialog_version"
@ -114,7 +110,6 @@ dependencies {
def glide_version = '4.13.2' def glide_version = '4.13.2'
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$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' implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
@ -125,7 +120,6 @@ dependencies {
implementation "dev.chrisbanes.insetter:insetter:0.6.1" 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.github.Adonai:jaudiotagger:2.3.15'
implementation 'com.r0adkll:slidableactivity:2.1.0' implementation 'com.r0adkll:slidableactivity:2.1.0'
implementation 'com.heinrichreimersoftware:material-intro:2.0.0' implementation 'com.heinrichreimersoftware:material-intro:2.0.0'

View file

@ -15,8 +15,6 @@
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> 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.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission <uses-permission

View file

@ -18,20 +18,12 @@ import android.provider.BaseColumns
import android.provider.MediaStore import android.provider.MediaStore
object Constants { 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 TRANSLATE = "https://crowdin.com/project/retromusicplayer"
const val WEBSITE = "https://retromusic.app"
const val GITHUB_PROJECT = "https://github.com/MuntashirAkon/Metro" const val GITHUB_PROJECT = "https://github.com/MuntashirAkon/Metro"
const val TELEGRAM_CHANGE_LOG = "https://t.me/AppManagerChannel" const val TELEGRAM_CHANGE_LOG = "https://t.me/AppManagerChannel"
const val USER_PROFILE = "profile.jpg" const val USER_PROFILE = "profile.jpg"
const val USER_BANNER = "banner.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 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 = const val IS_MUSIC =
MediaStore.Audio.AudioColumns.IS_MUSIC + "=1" + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''" 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 NEXT_SLEEP_TIMER_ELAPSED_REALTIME = "next_sleep_timer_elapsed_real_time"
const val IGNORE_MEDIA_STORE_ARTWORK = "ignore_media_store_artwork" const val IGNORE_MEDIA_STORE_ARTWORK = "ignore_media_store_artwork"
const val LAST_CHANGELOG_VERSION = "last_changelog_version" 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 START_DIRECTORY = "start_directory"
const val RECENTLY_PLAYED_CUTOFF = "recently_played_interval" const val RECENTLY_PLAYED_CUTOFF = "recently_played_interval"
const val LOCK_SCREEN = "lock_screen" 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.genres.GenreDetailsViewModel
import io.github.muntashirakon.music.fragments.playlists.PlaylistDetailsViewModel import io.github.muntashirakon.music.fragments.playlists.PlaylistDetailsViewModel
import io.github.muntashirakon.music.model.Genre 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.repository.*
import io.github.muntashirakon.music.util.FilePathUtil import io.github.muntashirakon.music.util.FilePathUtil
import kotlinx.coroutines.Dispatchers.IO 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.bind
import org.koin.dsl.module import org.koin.dsl.module
val networkModule = module {
factory {
provideDefaultCache()
}
factory {
provideOkHttp(get(), get())
}
single {
provideLastFmRetrofit(get())
}
single {
provideLastFmRest(get())
}
}
private val roomModule = module { private val roomModule = module {
single { single {
@ -117,7 +97,6 @@ private val dataModule = module {
get(), get(),
get(), get(),
get(), get(),
get(),
) )
} bind Repository::class } 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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem 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.content.getSystemService
import androidx.core.net.toUri 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.TintHelper
import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper import code.name.monkey.appthemehelper.util.ToolbarContentTintHelper
import io.github.muntashirakon.music.R import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.base.AbsThemeActivity 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.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.databinding.ActivityBugReportBinding
import io.github.muntashirakon.music.extensions.accentColor import io.github.muntashirakon.music.extensions.accentColor
import io.github.muntashirakon.music.extensions.setTaskDescriptionColorAuto import io.github.muntashirakon.music.extensions.setTaskDescriptionColorAuto
import io.github.muntashirakon.music.extensions.showToast 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() { open class BugReportActivity : AbsThemeActivity() {
@ -91,65 +55,14 @@ open class BugReportActivity : AbsThemeActivity() {
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
ToolbarContentTintHelper.colorBackButton(binding.toolbar) ToolbarContentTintHelper.colorBackButton(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) 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() } binding.cardDeviceInfo.airTextDeviceInfo.setOnClickListener { copyDeviceInfoToClipBoard() }
TintHelper.setTintAuto(binding.sendFab, accentColor, true) TintHelper.setTintAuto(binding.sendFab, accentColor, true)
binding.sendFab.setOnClickListener { reportIssue() } 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() { 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) val i = Intent(Intent.ACTION_VIEW)
@ -157,7 +70,6 @@ open class BugReportActivity : AbsThemeActivity() {
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(i) startActivity(i)
} }
}
private fun copyDeviceInfoToClipBoard() { private fun copyDeviceInfoToClipBoard() {
val clipboard = getSystemService<ClipboardManager>() val clipboard = getSystemService<ClipboardManager>()
@ -166,67 +78,6 @@ open class BugReportActivity : AbsThemeActivity() {
showToast(R.string.copied_device_info_to_clipboard) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) { if (item.itemId == android.R.id.home) {
onBackPressed() onBackPressed()
@ -234,72 +85,6 @@ open class BugReportActivity : AbsThemeActivity() {
return super.onOptionsItemSelected(item) 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() { private fun tryToFinishActivity() {
if (!isFinishing) { if (!isFinishing) {
finish() finish()
@ -307,10 +92,7 @@ open class BugReportActivity : AbsThemeActivity() {
} }
companion object { companion object {
private const val STATUS_BAD_CREDENTIALS = 401
private const val STATUS_ISSUES_NOT_ENABLED = 410
private const val ISSUE_TRACKER_LINK = 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.activity.addCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras 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.interfaces.ICabHolder
import io.github.muntashirakon.music.model.Album import io.github.muntashirakon.music.model.Album
import io.github.muntashirakon.music.model.Artist 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.repository.RealRepository
import io.github.muntashirakon.music.util.MusicUtil import io.github.muntashirakon.music.util.MusicUtil
import io.github.muntashirakon.music.util.PreferenceUtil import io.github.muntashirakon.music.util.PreferenceUtil
import io.github.muntashirakon.music.util.RetroColorUtil 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.AttachedCab
import com.afollestad.materialcab.attached.destroy import com.afollestad.materialcab.attached.destroy
import com.afollestad.materialcab.attached.isActive 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) { requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
if (!handleBackPress()) { if (!handleBackPress()) {
remove() remove()
@ -240,21 +228,6 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det
loadArtistImage(it) 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>) { private fun moreAlbums(albums: List<Album>) {
@ -274,41 +247,13 @@ class AlbumDetailsFragment : AbsMainActivityFragment(R.layout.fragment_album_det
binding.fragmentAlbumContent.moreRecyclerView.adapter = albumAdapter 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) { private fun loadArtistImage(artist: Artist) {
detailsViewModel.getMoreAlbums(artist).observe(viewLifecycleOwner) { detailsViewModel.getMoreAlbums(artist).observe(viewLifecycleOwner) {
moreAlbums(it) moreAlbums(it)
} }
GlideApp.with(requireContext()) GlideApp.with(requireContext())
//.forceDownload(PreferenceUtil.isAllowedToDownloadMetadata())
.load( .load(
RetroGlideExtension.getArtistModel( RetroGlideExtension.getArtistModel(artist)
artist,
PreferenceUtil.isAllowedToDownloadMetadata(requireContext())
)
) )
.artistImageOptions(artist) .artistImageOptions(artist)
.dontAnimate() .dontAnimate()

View file

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

View file

@ -13,9 +13,7 @@ import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController 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.ICabCallback
import io.github.muntashirakon.music.interfaces.ICabHolder import io.github.muntashirakon.music.interfaces.ICabHolder
import io.github.muntashirakon.music.model.Artist 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.repository.RealRepository
import io.github.muntashirakon.music.util.* import io.github.muntashirakon.music.util.*
import com.afollestad.materialcab.attached.AttachedCab 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 songAdapter: SimpleSongAdapter
private lateinit var albumAdapter: HorizontalAlbumAdapter private lateinit var albumAdapter: HorizontalAlbumAdapter
private var forceDownload: Boolean = false private var forceDownload: Boolean = false
private var lang: String? = null
private var biography: Spanned? = null
private val savedSongSortOrder: String private val savedSongSortOrder: String
get() = PreferenceUtil.artistDetailSongSortOrder get() = PreferenceUtil.artistDetailSongSortOrder
@ -105,14 +99,6 @@ abstract class AbsArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragm
setOnClickListener { MusicPlayerRemote.openAndShuffleQueue(artist.songs, true) } 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) { requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
if (!handleBackPress()) { if (!handleBackPress()) {
remove() remove()
@ -146,9 +132,6 @@ abstract class AbsArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragm
} }
this.artist = artist this.artist = artist
loadArtistImage(artist) loadArtistImage(artist)
if (PreferenceUtil.isAllowedToDownloadMetadata(requireContext())) {
loadBiography(artist.name)
}
binding.artistTitle.text = artist.name binding.artistTitle.text = artist.name
binding.text.text = String.format( binding.text.text = String.format(
"%s • %s", "%s • %s",
@ -171,51 +154,6 @@ abstract class AbsArtistDetailsFragment : AbsMainActivityFragment(R.layout.fragm
albumAdapter.swapDataSet(artist.albums) 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) { private fun loadArtistImage(artist: Artist) {
GlideApp.with(requireContext()).asBitmapPalette().artistImageOptions(artist) GlideApp.with(requireContext()).asBitmapPalette().artistImageOptions(artist)
.load(RetroGlideExtension.getArtistModel(artist)) .load(RetroGlideExtension.getArtistModel(artist))

View file

@ -17,8 +17,6 @@ package io.github.muntashirakon.music.fragments.artists
import androidx.lifecycle.* import androidx.lifecycle.*
import io.github.muntashirakon.music.interfaces.IMusicServiceEventListener import io.github.muntashirakon.music.interfaces.IMusicServiceEventListener
import io.github.muntashirakon.music.model.Artist 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.repository.RealRepository
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,16 +42,6 @@ class ArtistDetailsViewModel(
fun getArtist(): LiveData<Artist> = artistDetails 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() { override fun onMediaStoreChanged() {
fetchArtist() fetchArtist()
} }

View file

@ -15,9 +15,6 @@
package io.github.muntashirakon.music.fragments.settings package io.github.muntashirakon.music.fragments.settings
import android.os.Bundle 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 import io.github.muntashirakon.music.R
/** /**
@ -25,22 +22,9 @@ import io.github.muntashirakon.music.R
*/ */
class ImageSettingFragment : AbsSettingsFragment() { class ImageSettingFragment : AbsSettingsFragment() {
override fun invalidateSettings() { override fun invalidateSettings() {}
val autoDownloadImagesPolicy: Preference = findPreference(AUTO_DOWNLOAD_IMAGES_POLICY)!!
setSummary(autoDownloadImagesPolicy)
autoDownloadImagesPolicy.setOnPreferenceChangeListener { _, o ->
setSummary(autoDownloadImagesPolicy, o)
true
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_images) 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 package io.github.muntashirakon.music.glide.artistimage
import android.content.Context 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.Priority
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.GlideUrl import io.github.muntashirakon.music.util.MusicUtil
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream import java.io.InputStream
class ArtistImageFetcher( class ArtistImageFetcher(
private val context: Context, private val context: Context,
private val deezerService: DeezerService,
val model: ArtistImage, val model: ArtistImage,
private val okhttp: OkHttpClient
) : DataFetcher<InputStream> { ) : DataFetcher<InputStream> {
private var streamFetcher: OkHttpStreamFetcher? = null
private var response: Call<DeezerResponse>? = null
private var isCancelled: Boolean = false
override fun getDataClass(): Class<InputStream> { override fun getDataClass(): Class<InputStream> {
return InputStream::class.java return InputStream::class.java
} }
@ -40,53 +23,9 @@ class ArtistImageFetcher(
} }
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
try { if (!MusicUtil.isArtistNameUnknown(model.artist.name)) {
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()) 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)
}
} }
private fun getFallbackAlbumImage(): InputStream? { private fun getFallbackAlbumImage(): InputStream? {
@ -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() { override fun cleanup() {
streamFetcher?.cleanup()
} }
override fun cancel() { override fun cancel() {
isCancelled = true
response?.cancel()
streamFetcher?.cancel()
} }
} }

View file

@ -15,23 +15,16 @@
package io.github.muntashirakon.music.glide.artistimage package io.github.muntashirakon.music.glide.artistimage
import android.content.Context import android.content.Context
import io.github.muntashirakon.music.network.DeezerService
import com.bumptech.glide.load.Options import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoader.LoadData import com.bumptech.glide.load.model.ModelLoader.LoadData
import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit
class ArtistImageLoader( class ArtistImageLoader(
val context: Context, val context: Context,
private val deezerService: DeezerService,
private val okhttp: OkHttpClient
) : ModelLoader<ArtistImage, InputStream> { ) : ModelLoader<ArtistImage, InputStream> {
override fun buildLoadData( override fun buildLoadData(
@ -42,7 +35,7 @@ class ArtistImageLoader(
): LoadData<InputStream> { ): LoadData<InputStream> {
return LoadData( return LoadData(
ObjectKey(model.artist.name), ObjectKey(model.artist.name),
ArtistImageFetcher(context, deezerService, model, okhttp) ArtistImageFetcher(context, model)
) )
} }
@ -55,44 +48,9 @@ class Factory(
val context: Context val context: Context
) : ModelLoaderFactory<ArtistImage, InputStream> { ) : 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> { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ArtistImage, InputStream> {
return ArtistImageLoader( return ArtistImageLoader(context)
context,
deezerService,
okHttp
)
} }
override fun teardown() {} 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.fragments.search.Filter
import io.github.muntashirakon.music.model.* import io.github.muntashirakon.music.model.*
import io.github.muntashirakon.music.model.smartplaylist.NotPlayedPlaylist 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 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 { 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 historySong(): List<HistoryEntity>
fun favorites(): LiveData<List<SongEntity>> fun favorites(): LiveData<List<SongEntity>>
fun observableHistorySongs(): LiveData<List<Song>> fun observableHistorySongs(): LiveData<List<Song>>
@ -55,8 +40,6 @@ interface Repository {
suspend fun search(query: String?, filter: Filter): MutableList<Any> suspend fun search(query: String?, filter: Filter): MutableList<Any>
suspend fun getPlaylistSongs(playlist: Playlist): List<Song> suspend fun getPlaylistSongs(playlist: Playlist): List<Song>
suspend fun getGenre(genreId: Long): 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 artistById(artistId: Long): Artist
suspend fun albumArtistByName(name: String): Artist suspend fun albumArtistByName(name: String): Artist
suspend fun recentArtists(): List<Artist> suspend fun recentArtists(): List<Artist>
@ -74,8 +57,6 @@ interface Repository {
suspend fun playlists(): Home suspend fun playlists(): Home
suspend fun homeSections(): List<Home> suspend fun homeSections(): List<Home>
@ExperimentalCoroutinesApi
suspend fun homeSectionsFlow(): Flow<Result<List<Home>>>
suspend fun playlist(playlistId: Long): Playlist suspend fun playlist(playlistId: Long): Playlist
suspend fun fetchPlaylistWithSongs(): List<PlaylistWithSongs> suspend fun fetchPlaylistWithSongs(): List<PlaylistWithSongs>
suspend fun playlistSongs(playlistWithSongs: PlaylistWithSongs): List<Song> suspend fun playlistSongs(playlistWithSongs: PlaylistWithSongs): List<Song>
@ -115,7 +96,6 @@ interface Repository {
class RealRepository( class RealRepository(
private val context: Context, private val context: Context,
private val lastFMService: LastFMService,
private val songRepository: SongRepository, private val songRepository: SongRepository,
private val albumRepository: AlbumRepository, private val albumRepository: AlbumRepository,
private val artistRepository: ArtistRepository, private val artistRepository: ArtistRepository,
@ -183,59 +163,6 @@ class RealRepository(
override suspend fun getGenre(genreId: Long): List<Song> = genreRepository.songs(genreId) 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> { override suspend fun homeSections(): List<Home> {
val homeSections = mutableListOf<Home>() val homeSections = mutableListOf<Home>()
val sections: List<Home> = listOf( val sections: List<Home> = listOf(
@ -420,53 +347,4 @@ class RealRepository(
return Home(songs, FAVOURITES, R.string.favorites) 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 package io.github.muntashirakon.music.util
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.core.content.res.use import androidx.core.content.res.use
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -115,12 +111,6 @@ object PreferenceUtil {
putString(SAF_SDCARD_URI, value) putString(SAF_SDCARD_URI, value)
} }
val autoDownloadImagesPolicy
get() = sharedPreferences.getStringOrDefault(
AUTO_DOWNLOAD_IMAGES_POLICY,
"only_wifi"
)
var albumArtistsOnly var albumArtistsOnly
get() = sharedPreferences.getBoolean( get() = sharedPreferences.getBoolean(
ALBUM_ARTISTS_ONLY, ALBUM_ARTISTS_ONLY,
@ -336,26 +326,6 @@ object PreferenceUtil {
val isLockScreen get() = sharedPreferences.getBoolean(LOCK_SCREEN, false) 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 var lyricsOption
get() = sharedPreferences.getInt(LYRICS_OPTIONS, 1) get() = sharedPreferences.getInt(LYRICS_OPTIONS, 1)
set(value) = sharedPreferences.edit { set(value) = sharedPreferences.edit {

View file

@ -33,13 +33,6 @@
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical"> 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 <include
android:id="@+id/card_device_info" android:id="@+id/card_device_info"
layout="@layout/bug_report_card_device_info" layout="@layout/bug_report_card_device_info"
@ -60,5 +53,5 @@
style="@style/Fab" style="@style/Fab"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="16dp" android:layout_margin="16dp"
app:srcCompat="@drawable/ic_send" /> app:srcCompat="@drawable/ic_open_in_browser" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </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:listitem="@layout/item_album_card"
tools:visibility="visible" /> 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 <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="72dp" android:layout_height="72dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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> </io.github.muntashirakon.music.views.insets.InsetsConstraintLayout>

View file

@ -107,106 +107,9 @@
app:layout_constraintTop_toBottomOf="@id/songTitle" app:layout_constraintTop_toBottomOf="@id/songTitle"
tools:listitem="@layout/item_song" /> 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 <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="72dp" android:layout_height="72dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/listeners" /> app:layout_constraintTop_toBottomOf="@id/recyclerView" />
</io.github.muntashirakon.music.views.insets.InsetsConstraintLayout> </io.github.muntashirakon.music.views.insets.InsetsConstraintLayout>

View file

@ -10,11 +10,4 @@
android:title="@string/pref_title_ignore_media_store_artwork" android:title="@string/pref_title_ignore_media_store_artwork"
app:icon="@drawable/ic_image" /> 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> </androidx.preference.PreferenceScreen>