Massive, project-wide warning cleanup.

This commit is contained in:
casey langen 2019-02-10 11:05:03 -08:00
parent 60a55f0cff
commit 89fc15fe72
23 changed files with 301 additions and 264 deletions

View File

@ -10,6 +10,7 @@ import io.casey.musikcube.remote.injection.ServiceModule
import io.casey.musikcube.remote.service.gapless.GaplessHeaderService
import io.casey.musikcube.remote.service.playback.impl.streaming.db.OfflineDb
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.extension.getString
import io.fabric.sdk.android.Fabric
import java.util.*
import javax.inject.Inject
@ -24,7 +25,7 @@ class Application : android.app.Application() {
super.onCreate()
val prefs = getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
deviceId = prefs.getString(Prefs.Key.DEVICE_ID, "")
deviceId = prefs.getString(Prefs.Key.DEVICE_ID) ?: ""
if (deviceId.isBlank()) {
deviceId = UUID.randomUUID().toString()
prefs.edit().putString(Prefs.Key.DEVICE_ID, deviceId).apply()

View File

@ -11,7 +11,7 @@ class MixinSet : MixinBase() {
private var bundle = Bundle()
fun <T> add(mixin: IMixin): T {
components.put(mixin.javaClass, mixin)
components[mixin.javaClass] = mixin
when (state) {
State.Created ->
@ -33,10 +33,12 @@ class MixinSet : MixinBase() {
}
}
@Suppress("unchecked_cast")
return mixin as T
}
fun <T: IMixin> get(cls: Class<out T>): T? = components.get(cls) as T?
@Suppress("unchecked_cast")
fun <T: IMixin> get(cls: Class<out T>): T? = components[cls] as T?
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)

View File

@ -73,6 +73,7 @@ abstract class ViewModel<T>(protected val runner: Runner? = null): Runner.TaskCa
private val idToInstance = mutableMapOf<Long, ViewModel<*>>()
fun <T: ViewModel<*>> restore(id: Long): T? {
@Suppress("unchecked_cast")
val instance: T? = idToInstance[id] as T?
if (instance != null) {
handler.removeCallbacks(instance.cleanup)

View File

@ -12,8 +12,8 @@ import com.bumptech.glide.module.AppGlideModule
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import okhttp3.*
import java.io.InputStream
import io.casey.musikcube.remote.ui.shared.model.albumart.canIntercept as canInterceptArtwork
import io.casey.musikcube.remote.ui.shared.model.albumart.intercept as interceptArtwork
import io.casey.musikcube.remote.ui.shared.util.AlbumArtLookup.canIntercept as canInterceptArtwork
import io.casey.musikcube.remote.ui.shared.util.AlbumArtLookup.intercept as interceptArtwork
@GlideModule
class GlideModule : AppGlideModule() {

View File

@ -13,7 +13,6 @@ import io.casey.musikcube.remote.service.gapless.db.GaplessDb
import io.casey.musikcube.remote.service.gapless.db.GaplessTrack
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.util.NetworkUtil
import io.casey.musikcube.remote.util.Strings
import java.io.File
import java.util.*
import javax.inject.Inject
@ -78,11 +77,11 @@ class StreamProxy(private val context: Context) {
proxy = HttpProxyCacheServer.Builder(context.applicationContext)
.cacheDirectory(cachePath)
.maxCacheSize(CACHE_SETTING_TO_BYTES[diskCacheIndex] ?: MINIMUM_CACHE_SIZE_BYTES)
.headerInjector { _ ->
.headerInjector {
val headers = HashMap<String, String>()
val userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD)!!
val encoded = Base64.encodeToString(userPass.toByteArray(), Base64.NO_WRAP)
headers.put("Authorization", "Basic " + encoded)
headers["Authorization"] = "Basic $encoded"
headers
}
.headerReceiver { url: String, headers: Map<String, List<String>> ->
@ -105,10 +104,10 @@ class StreamProxy(private val context: Context) {
}
companion object {
private val BYTES_PER_MEGABYTE = 1048576L
private val BYTES_PER_GIGABYTE = 1073741824L
private val ESTIMATED_LENGTH = "X-musikcube-Estimated-Content-Length"
val MINIMUM_CACHE_SIZE_BYTES = BYTES_PER_MEGABYTE * 128
private const val BYTES_PER_MEGABYTE = 1048576L
private const val BYTES_PER_GIGABYTE = 1073741824L
private const val ESTIMATED_LENGTH = "X-musikcube-Estimated-Content-Length"
const val MINIMUM_CACHE_SIZE_BYTES = BYTES_PER_MEGABYTE * 128
val CACHE_SETTING_TO_BYTES: MutableMap<Int, Long> = mutableMapOf(
0 to MINIMUM_CACHE_SIZE_BYTES,
@ -127,15 +126,14 @@ class StreamProxy(private val context: Context) {
val segments = uri.pathSegments
if (segments.size == 3 && "external_id" == segments[1]) {
/* url params, hyphen separated */
var params = uri.query
if (Strings.notEmpty(params)) {
params = "-" + params
.replace("?", "-")
.replace("&", "-")
.replace("=", "-")
}
else {
params = ""
val params = when (uri?.query.isNullOrBlank()) {
true -> ""
false ->
"-" + uri!!.query!!
.replace("?", "-")
.replace("&", "-")
.replace("=", "-")
}
return@gen "${segments[2]}$params"

View File

@ -184,7 +184,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
val context = QueryContext(Messages.Request.PlaySnapshotTracks)
val type = PlayQueueType.Snapshot
service.queryContext?.let { _ ->
service.queryContext?.let {
dataProvider.snapshotPlayQueue().subscribeBy(
onNext = {
resetPlayContextAndQueryFactory()
@ -347,7 +347,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
override var state = PlaybackState.Stopped
private set(value) {
if (field !== value) {
Log.d(TAG, "state = " + state)
Log.d(TAG, "state=$state")
field = value
notifyEventListeners()
}
@ -366,10 +366,10 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
override fun toggleRepeatMode() {
when (repeatMode) {
RepeatMode.None -> repeatMode = RepeatMode.List
RepeatMode.List -> repeatMode = RepeatMode.Track
else -> repeatMode = RepeatMode.None
repeatMode = when (repeatMode) {
RepeatMode.None -> RepeatMode.List
RepeatMode.List -> RepeatMode.Track
else -> RepeatMode.None
}
this.prefs.edit().putString(REPEAT_MODE_PREF, repeatMode.toString()).apply()
@ -585,13 +585,10 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
else if (!userInitiated && repeatMode === RepeatMode.Track) {
return currentIndex
}
else {
if (currentIndex + 1 >= count) {
return if (repeatMode === RepeatMode.List) 0 else -1
}
else {
return currentIndex + 1
}
return when (currentIndex + 1 >= count) {
true -> if (repeatMode === RepeatMode.List) 0 else -1
false -> currentIndex + 1
}
}
@ -714,6 +711,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
val countMessage = playlistQueryFactory.count() ?: return
@Suppress("unused")
countMessage
.concatMap { count ->
getCurrentAndNextTrackMessages(playContext, count)
@ -763,6 +761,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
val query = playlistQueryFactory.page(start, count)
if (query != null) {
@Suppress("unused")
query.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(

View File

@ -33,11 +33,11 @@ import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.home.activity.MainActivity
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.model.albumart.Size
import io.casey.musikcube.remote.ui.shared.util.Size
import io.casey.musikcube.remote.util.Debouncer
import io.casey.musikcube.remote.util.Strings
import android.support.v4.app.NotificationCompat.Action as NotifAction
import io.casey.musikcube.remote.ui.shared.model.albumart.getUrl as getAlbumArtUrl
import io.casey.musikcube.remote.ui.shared.util.AlbumArtLookup.getUrl as getAlbumArtUrl
/**
* a service used to interact with all of the system media-related components -- notifications,

View File

@ -10,6 +10,7 @@ import android.util.Log
import com.neovisionaries.ws.client.*
import io.casey.musikcube.remote.BuildConfig
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.extension.getString
import io.casey.musikcube.remote.ui.shared.util.NetworkUtil
import io.casey.musikcube.remote.util.Preconditions
import io.reactivex.Observable
@ -137,7 +138,7 @@ class WebSocketService constructor(private val context: Context) {
private set(newState) {
Preconditions.throwIfNotOnMainThread()
Log.d(TAG, "state = " + newState)
Log.d(TAG, "state=$newState")
if (state == State.Disconnected) {
serverVersion = -1
@ -158,6 +159,7 @@ class WebSocketService constructor(private val context: Context) {
interceptors.add(interceptor)
}
@Suppress("unused")
fun removeInterceptor(interceptor: (SocketMessage, Responder) -> Boolean) {
Preconditions.throwIfNotOnMainThread()
interceptors.remove(interceptor)
@ -230,7 +232,7 @@ class WebSocketService constructor(private val context: Context) {
val ping = SocketMessage.Builder.request(Messages.Request.Ping).build()
send(ping, INTERNAL_CLIENT) { _: SocketMessage ->
send(ping, INTERNAL_CLIENT) {
handler.removeMessages(MESSAGE_PING_TIMEOUT)
handler.sendEmptyMessageDelayed(MESSAGE_SCHEDULE_PING, PING_INTERVAL_MILLIS)
}
@ -239,7 +241,9 @@ class WebSocketService constructor(private val context: Context) {
fun cancelMessages(client: Client) {
Preconditions.throwIfNotOnMainThread()
removeCallbacks({ mrd: MessageResultDescriptor -> mrd.client === client })
removeCallbacks { mrd: MessageResultDescriptor ->
mrd.client === client
}
}
fun send(message: SocketMessage,
@ -282,14 +286,12 @@ class WebSocketService constructor(private val context: Context) {
mrd.callback = callback
mrd.type = Type.Callback
mrd.intercepted = intercepted
messageCallbacks.put(message.id, mrd)
messageCallbacks[message.id] = mrd
}
if (!intercepted) {
socket?.sendText(message.toString())
}
else {
Log.d(TAG, "send: message intercepted with id " + id.toString())
when (intercepted) {
true -> Log.d(TAG, "send: message intercepted with id=$id")
false -> socket?.sendText(message.toString())
}
return id
@ -346,21 +348,22 @@ class WebSocketService constructor(private val context: Context) {
subject.onError(ex)
}
@Suppress("unused")
subject.doOnDispose { cancelMessage(mrd.id) }
if (!intercepted) {
socket?.sendText(message.toString())
}
messageCallbacks.put(message.id, mrd)
messageCallbacks[message.id] = mrd
return subject
}
fun hasValidConnection(): Boolean {
val addr = prefs.getString(Prefs.Key.ADDRESS, "")
val address = prefs.getString(Prefs.Key.ADDRESS) ?: ""
val port = prefs.getInt(Prefs.Key.MAIN_PORT, -1)
return addr.isNotEmpty() && port >= 0
return address.isNotEmpty() && port >= 0
}
private fun disconnect(autoReconnect: Boolean) {
@ -390,25 +393,27 @@ class WebSocketService constructor(private val context: Context) {
}
}
private fun removeNonInterceptedCallbacks() {
removeCallbacks({ mrd -> !mrd.intercepted })
}
private fun removeNonInterceptedCallbacks() =
removeCallbacks {
mrd -> !mrd.intercepted
}
private fun removeInternalCallbacks() {
removeCallbacks({ mrd: MessageResultDescriptor -> mrd.client === INTERNAL_CLIENT })
}
private fun removeInternalCallbacks() =
removeCallbacks {
mrd: MessageResultDescriptor -> mrd.client === INTERNAL_CLIENT
}
private fun removeExpiredCallbacks() {
val now = System.currentTimeMillis()
removeCallbacks({ mrd: MessageResultDescriptor -> now - mrd.enqueueTime > CALLBACK_TIMEOUT_MILLIS })
removeCallbacks {
mrd: MessageResultDescriptor -> now - mrd.enqueueTime > CALLBACK_TIMEOUT_MILLIS
}
}
private fun removeCallbacksForClient(client: Client) {
removeCallbacks({ mrd: MessageResultDescriptor ->
private fun removeCallbacksForClient(client: Client) =
removeCallbacks { mrd: MessageResultDescriptor ->
mrd.client === client
})
}
}
private fun removeCallbacks(predicate: (MessageResultDescriptor) -> Boolean) {
val it = messageCallbacks.entries.iterator()
@ -595,25 +600,25 @@ class WebSocketService constructor(private val context: Context) {
}
companion object {
private val TAG = "WebSocketService"
private const val TAG = "WebSocketService"
private val AUTO_RECONNECT_INTERVAL_MILLIS = 2000L
private val CALLBACK_TIMEOUT_MILLIS = 30000L
private val CONNECTION_TIMEOUT_MILLIS = 5000
private val PING_INTERVAL_MILLIS = 3500L
private val AUTO_CONNECT_FAILSAFE_DELAY_MILLIS = 2000L
private val AUTO_DISCONNECT_DELAY_MILLIS = 10000L
private val FLAG_AUTHENTICATION_FAILED = 0xbeef
private val WEBSOCKET_FLAG_POLICY_VIOLATION = 1008
private val MINIMUM_SUPPORTED_API_VERSION = 15
private const val AUTO_RECONNECT_INTERVAL_MILLIS = 2000L
private const val CALLBACK_TIMEOUT_MILLIS = 30000L
private const val CONNECTION_TIMEOUT_MILLIS = 5000
private const val PING_INTERVAL_MILLIS = 3500L
private const val AUTO_CONNECT_FAILSAFE_DELAY_MILLIS = 2000L
private const val AUTO_DISCONNECT_DELAY_MILLIS = 10000L
private const val FLAG_AUTHENTICATION_FAILED = 0xbeef
private const val WEBSOCKET_FLAG_POLICY_VIOLATION = 1008
private const val MINIMUM_SUPPORTED_API_VERSION = 15
private val MESSAGE_BASE = 0xcafedead.toInt()
private val MESSAGE_CONNECT_THREAD_FINISHED = MESSAGE_BASE + 0
private val MESSAGE_RECEIVED = MESSAGE_BASE + 1
private val MESSAGE_REMOVE_OLD_CALLBACKS = MESSAGE_BASE + 2
private val MESSAGE_AUTO_RECONNECT = MESSAGE_BASE + 3
private val MESSAGE_SCHEDULE_PING = MESSAGE_BASE + 4
private val MESSAGE_PING_TIMEOUT = MESSAGE_BASE + 5
private const val MESSAGE_BASE = 0xcafedead.toInt()
private const val MESSAGE_CONNECT_THREAD_FINISHED = MESSAGE_BASE + 0
private const val MESSAGE_RECEIVED = MESSAGE_BASE + 1
private const val MESSAGE_REMOVE_OLD_CALLBACKS = MESSAGE_BASE + 2
private const val MESSAGE_AUTO_RECONNECT = MESSAGE_BASE + 3
private const val MESSAGE_SCHEDULE_PING = MESSAGE_BASE + 4
private const val MESSAGE_PING_TIMEOUT = MESSAGE_BASE + 5
private val DISCONNECT_ON_PING_TIMEOUT = !BuildConfig.DEBUG

View File

@ -17,8 +17,8 @@ import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.extension.titleEllipsizeMode
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.albumart.Size
import io.casey.musikcube.remote.ui.shared.model.albumart.getUrl
import io.casey.musikcube.remote.ui.shared.util.Size
import io.casey.musikcube.remote.ui.shared.util.AlbumArtLookup.getUrl
class AlbumBrowseAdapter(private val listener: EventListener,
private val playback: PlaybackMixin,

View File

@ -39,12 +39,12 @@ import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.model.albumart.Size
import io.casey.musikcube.remote.ui.shared.util.Size
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
import io.casey.musikcube.remote.util.Strings
import org.json.JSONArray
import javax.inject.Inject
import io.casey.musikcube.remote.ui.shared.model.albumart.getUrl as getAlbumArtUrl
import io.casey.musikcube.remote.ui.shared.util.AlbumArtLookup.getUrl as getAlbumArtUrl
class MainMetadataView : FrameLayout {
@Inject lateinit var wss: WebSocketService

View File

@ -22,6 +22,7 @@ import io.casey.musikcube.remote.ui.settings.model.Connection
import io.casey.musikcube.remote.ui.settings.model.ConnectionsDb
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.extension.*
import java.lang.IllegalArgumentException
import javax.inject.Inject
private const val EXTRA_CONNECTION = "extra_connection"
@ -197,19 +198,22 @@ private class RenameTask(val db: ConnectionsDb, val connection: Connection, val
class ConfirmDeleteDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val connection = arguments!!.getParcelable<Connection>(EXTRA_CONNECTION)
val message = getString(R.string.settings_confirm_delete_message, connection.name)
val dlg = AlertDialog.Builder(activity!!)
.setTitle(R.string.settings_confirm_delete_title)
.setMessage(message)
.setNegativeButton(R.string.button_no, null)
.setPositiveButton(R.string.button_yes) { _, _ ->
(activity as ConnectionsActivity).delete(connection)
return when (connection == null) {
true -> throw IllegalArgumentException("invalid connection")
else -> {
AlertDialog.Builder(activity!!)
.setTitle(R.string.settings_confirm_delete_title)
.setMessage(getString(R.string.settings_confirm_delete_message, connection.name))
.setNegativeButton(R.string.button_no, null)
.setPositiveButton(R.string.button_yes) { _, _ ->
(activity as ConnectionsActivity).delete(connection)
}
.create().apply {
setCancelable(false)
}
}
.create()
dlg.setCancelable(false)
return dlg
}
}
companion object {
@ -228,26 +232,29 @@ class RenameDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val connection = arguments!!.getParcelable<Connection>(EXTRA_CONNECTION)
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.dialog_edit, null)
val edit = view.findViewById<EditText>(R.id.edit)
return when (connection == null) {
true -> throw IllegalArgumentException("invalid connection")
else -> {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.dialog_edit, null)
val edit = view.findViewById<EditText>(R.id.edit)
edit.setText(connection.name)
edit.selectAll()
edit.setText(connection.name)
edit.selectAll()
val dlg = AlertDialog.Builder(activity!!)
.setTitle(R.string.settings_save_as_title)
.setNegativeButton(R.string.button_cancel, null)
.setPositiveButton(R.string.button_save) { _, _ ->
val name = edit.text.toString()
(activity as ConnectionsActivity).rename(connection, name)
}
.create()
dlg.setView(view)
dlg.setCancelable(false)
return dlg
AlertDialog.Builder(activity!!)
.setTitle(R.string.settings_save_as_title)
.setNegativeButton(R.string.button_cancel, null)
.setPositiveButton(R.string.button_save) { _, _ ->
val name = edit.text.toString()
(activity as ConnectionsActivity).rename(connection, name)
}
.create().apply {
setView(view)
setCancelable(false)
}
}
}
}
override fun onResume() {

View File

@ -52,6 +52,7 @@ class RemoteEqActivity: BaseActivity() {
}
override fun <T : ViewModel<*>> createViewModel(): T? {
@Suppress("unchecked_cast")
return RemoteEqViewModel() as T
}

View File

@ -85,6 +85,7 @@ class RemoteSettingsActivity: BaseActivity() {
}
override fun <T : ViewModel<*>> createViewModel(): T? {
@Suppress("unchecked_cast")
return RemoteSettingsViewModel() as T
}
@ -209,6 +210,8 @@ class RemoteSettingsActivity: BaseActivity() {
ViewModelState.Saved -> {
finish()
}
else -> {
}
}
invalidateOptionsMenu()
}

View File

@ -24,6 +24,7 @@ import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import java.lang.IllegalArgumentException
import java.util.*
import javax.inject.Inject
import io.casey.musikcube.remote.ui.settings.constants.Prefs.Default as Defaults
@ -107,7 +108,7 @@ class SettingsActivity : BaseActivity() {
private fun rebindUi() {
/* connection info */
addressText.setTextAndMoveCursorToEnd(prefs.getString(Keys.ADDRESS, Defaults.ADDRESS))
addressText.setTextAndMoveCursorToEnd(prefs.getString(Keys.ADDRESS) ?: Defaults.ADDRESS)
portText.setTextAndMoveCursorToEnd(String.format(
Locale.ENGLISH, "%d", prefs.getInt(Keys.MAIN_PORT, Defaults.MAIN_PORT)))
@ -115,7 +116,7 @@ class SettingsActivity : BaseActivity() {
httpPortText.setTextAndMoveCursorToEnd(String.format(
Locale.ENGLISH, "%d", prefs.getInt(Keys.AUDIO_PORT, Defaults.AUDIO_PORT)))
passwordText.setTextAndMoveCursorToEnd(prefs.getString(Keys.PASSWORD, Defaults.PASSWORD))
passwordText.setTextAndMoveCursorToEnd(prefs.getString(Keys.PASSWORD) ?: Defaults.PASSWORD)
/* bitrate */
val bitrates = ArrayAdapter.createFromResource(
@ -402,10 +403,15 @@ class SettingsActivity : BaseActivity() {
.setMessage(R.string.settings_confirm_overwrite_message)
.setNegativeButton(R.string.button_no, null)
.setPositiveButton(R.string.button_yes) { _, _ ->
val connection = arguments!!.getParcelable<Connection>(EXTRA_CONNECTION)
val db = (activity as SettingsActivity).connectionsDb
val saveAs = SaveAsTask(db, connection, true)
(activity as SettingsActivity).runner.run(SaveAsTask.nameFor(connection), saveAs)
val connection = arguments?.getParcelable<Connection>(EXTRA_CONNECTION)
when (connection) {
null -> throw IllegalArgumentException("invalid connection")
else -> {
val db = (activity as SettingsActivity).connectionsDb
val saveAs = SaveAsTask(db, connection, true)
(activity as SettingsActivity).runner.run(SaveAsTask.nameFor(connection), saveAs)
}
}
}
.create()

View File

@ -18,9 +18,9 @@ class Connection : Parcelable {
constructor()
constructor(source: Parcel) {
name = source.readString()
hostname = source.readString()
password = source.readString()
name = source.readString() ?: ""
hostname = source.readString() ?: ""
password = source.readString() ?: ""
httpPort = source.readInt()
wssPort = source.readInt()
ssl = (source.readInt() == 1)
@ -49,6 +49,7 @@ class Connection : Parcelable {
}
companion object {
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<Connection> = object: Parcelable.Creator<Connection> {
override fun createFromParcel(source: Parcel?): Connection {

View File

@ -129,6 +129,9 @@ abstract class BaseActivity : AppCompatActivity(), ViewModel.Provider, Runner.Ta
protected open val transitionType = Transition.Horizontal
protected val extras: Bundle
get() = intent?.extras ?: Bundle()
override fun <T: ViewModel<*>> createViewModel(): T? = null
protected fun <T: ViewModel<*>> getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get<T>() as T
protected fun <T: IMixin> mixin(mixin: T): T = mixins.add(mixin)

View File

@ -237,10 +237,10 @@ fun AppCompatActivity.showSnackbar(viewId: Int, stringId: Int, buttonText: Strin
showSnackbar(this.findViewById<View>(viewId), stringId, buttonText, buttonCb)
fun fallback(input: String?, fallback: String): String =
if (input.isNullOrEmpty()) fallback else input!!
if (input.isNullOrEmpty()) fallback else input
fun fallback(input: String?, fallback: Int): String =
if (input.isNullOrEmpty()) Application.instance.getString(fallback) else input!!
if (input.isNullOrEmpty()) Application.instance.getString(fallback) else input
fun AppCompatActivity.slideNextUp() = overridePendingTransition(R.anim.slide_up, R.anim.stay_put)
@ -277,6 +277,12 @@ fun titleEllipsizeMode(prefs: SharedPreferences): TextUtils.TruncateAt {
}
}
fun SharedPreferences.getString(key: String): String? =
when (!this.contains(key)) {
true -> null
else -> this.getString(key, "")
}
inline fun <reified T> FragmentManager.find(tag: String): T {
return findFragmentByTag(tag) as T
}

View File

@ -12,7 +12,7 @@ import io.casey.musikcube.remote.ui.settings.constants.Prefs
class PlaybackMixin(var listener: (() -> Unit)? = { }): MixinBase() {
private lateinit var prefs: SharedPreferences
private val context = Application.instance!!
private val context = Application.instance
var service: IPlaybackService = PlaybackServiceFactory.instance(context)
private set

View File

@ -11,6 +11,7 @@ class ViewModelMixin(private val provider: ViewModel.Provider): MixinBase() {
if (viewModel == null) {
viewModel = provider.createViewModel()
}
@Suppress("unchecked_cast")
return viewModel as T?
}

View File

@ -42,7 +42,7 @@ abstract class BaseSlidingWindow(
disposables.add(dataProvider.observePlayQueue()
.subscribe({ requery() }, { /* error */ }))
recyclerView.setStateChangeListener(fastScrollStateChangeListener)
recyclerView.setOnFastScrollStateChangeListener(fastScrollStateChangeListener)
recyclerView.addOnScrollListener(recyclerViewScrollListener)
connected = true
fastScrollerActive = false

View File

@ -1,4 +1,4 @@
package io.casey.musikcube.remote.ui.shared.model.albumart
package io.casey.musikcube.remote.ui.shared.util
import android.content.Context
import android.util.LruCache
@ -44,47 +44,19 @@ private val badPatterns = arrayOf(
/* http://www.last.fm/group/Last.fm+Web+Services/forum/21604/_/522900 -- it's ok to
put our key in the code */
private val lastFmFormatUrl =
private const val LASTFM_FORMAT_URL =
"http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=" +
"502c69bd3f9946e8e0beee4fcb28c4cd&artist=%s&album=%s&format=json&size=%s"
private val urlCache = LruCache<String, String>(500)
private val badUrlCache = LruCache<String, Boolean>(100)
private val inFlight = mutableMapOf<String, CountDownLatch>()
private val httpClient = OkHttpClient.Builder().build()
private val prefs by lazy {
Application.instance.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
}
fun getUrl(album: IAlbum, size: Size = Size.Small): String? {
return getThumbnailUrl(album.thumbnailId)
?: getUrl(album.albumArtist, album.name, size)
}
fun getUrl(track: ITrack, size: Size = Size.Small): String? {
return getThumbnailUrl(track.thumbnailId)
?: getUrl(track.artist, track.album, size)
}
fun getUrl(artist: String = "", album: String = "", size: Size = Size.Small): String? {
if (!prefs.getBoolean(Prefs.Key.LASTFM_ENABLED, Prefs.Default.LASTFM_ENABLED)) {
return null
}
if (artist.isBlank() || album.isBlank()) {
return null
}
return String.format(lastFmFormatUrl, dejunk(artist), dejunk(album), size.key)
}
fun canIntercept(request: Request): Boolean {
return request.url().host() == "ws.audioscrobbler.com" &&
request.url().queryParameter("method") == "album.getinfo"
}
private val httpClient = OkHttpClient.Builder().build()
private fun executeWithRetries(req: Request): Response? {
var count = 0
var result: Response? = null
@ -103,109 +75,6 @@ private fun executeWithRetries(req: Request): Response? {
return result
}
fun intercept(req: Request): Request? {
val url = req.url()
var imageUrl = urlCache[url.toString()] ?: ""
val desiredSize = Size.from(url.queryParameter("size"))
var badUrl = false
var pending: CountDownLatch? = null
synchronized(urlCache) {
badUrl = badUrlCache.get(url.toString()) ?: false
pending = inFlight[url.toString()]
}
/* let's see if there's already another request for this URL in flight. if there is,
let it finish, as it'll wind up in the cache and we won't have to make multiple
requests against the backend */
if (pending != null) {
pending?.await()
pending = null
synchronized(urlCache) {
imageUrl = urlCache[url.toString()] ?: ""
}
}
/* depending on the above, we may have an imageUrl! if we do, we're good. otherwise,
let's setup the countdown latch so subsequent requests wait... */
if (imageUrl.isBlank() && !badUrl) {
synchronized(urlCache) {
pending = CountDownLatch(1)
inFlight.put(url.toString(), pending!!)
}
}
if (imageUrl.isBlank() && !badUrl) {
val response = executeWithRetries(req)
if (response != null) {
val images = mutableListOf<Pair<Size, String>>()
try {
val json = JSONObject(response.body()?.string())
val imagesJson = json.getJSONObject("album").getJSONArray("image")
for (i in 0 until imagesJson.length()) {
val imageJson = imagesJson.getJSONObject(i)
val size = Size.from(imageJson.optString("size", ""))
val resolvedUrl = imageJson.optString("#text", "")
if (Strings.notEmpty(resolvedUrl)) {
images.add(Pair<Size, String>(size, resolvedUrl))
}
}
}
catch (ex: JSONException) {
badUrlCache.put(url.toString(), true)
}
if (images.size > 0) {
/* find the image with the closest to the requested size.
exact match preferred. */
var closest = images[0]
var lastDelta = Integer.MAX_VALUE
for (check in images) {
if (check.first == desiredSize) {
closest = check
break
}
else {
val delta = Math.abs(desiredSize.order - check.first.order)
if (lastDelta > delta) {
closest = check
lastDelta = delta
}
}
}
imageUrl = closest.second
}
}
}
var result: Request? = null
if (imageUrl.isNotBlank()) {
synchronized(urlCache) {
urlCache.put(url.toString(), imageUrl)
}
if (desiredSize == Size.Mega) {
imageUrl = imageUrl.replace("/i/u/300x300", "/i/u/600x600")
}
result = Request.Builder().url(imageUrl).build()
}
synchronized(urlCache) {
if (pending != null) {
pending?.countDown()
inFlight.remove(url.toString())
}
}
return result
}
private fun getThumbnailUrl(id: Long): String? {
if (id > 0) {
@ -223,5 +92,138 @@ private fun dejunk(album: String): String {
for (pattern in badPatterns) {
result = pattern.matcher(result).replaceAll("")
}
@Suppress("deprecation")
return URLEncoder.encode(result.trim { it.isWhitespace() })
}
object AlbumArtLookup {
fun getUrl(album: IAlbum, size: Size = Size.Small): String? {
return getThumbnailUrl(album.thumbnailId)
?: getUrl(album.albumArtist, album.name, size)
}
fun getUrl(track: ITrack, size: Size = Size.Small): String? {
return getThumbnailUrl(track.thumbnailId)
?: getUrl(track.artist, track.album, size)
}
fun getUrl(artist: String = "", album: String = "", size: Size = Size.Small): String? {
if (!prefs.getBoolean(Prefs.Key.LASTFM_ENABLED, Prefs.Default.LASTFM_ENABLED)) {
return null
}
if (artist.isBlank() || album.isBlank()) {
return null
}
return String.format(LASTFM_FORMAT_URL, dejunk(artist), dejunk(album), size.key)
}
fun canIntercept(request: Request): Boolean {
return request.url().host() == "ws.audioscrobbler.com" &&
request.url().queryParameter("method") == "album.getinfo"
}
fun intercept(req: Request): Request? {
val url = req.url()
var imageUrl = urlCache[url.toString()] ?: ""
val desiredSize = Size.from(url.queryParameter("size"))
var badUrl: Boolean
var pending: CountDownLatch?
synchronized(urlCache) {
badUrl = badUrlCache.get(url.toString()) ?: false
pending = inFlight[url.toString()]
}
/* let's see if there's already another request for this URL in flight. if there is,
let it finish, as it'll wind up in the cache and we won't have to make multiple
requests against the backend */
if (pending != null) {
pending?.await()
pending = null
synchronized(urlCache) {
imageUrl = urlCache[url.toString()] ?: ""
}
}
/* depending on the above, we may have an imageUrl! if we do, we're good. otherwise,
let's setup the countdown latch so subsequent requests wait... */
if (imageUrl.isBlank() && !badUrl) {
synchronized(urlCache) {
pending = CountDownLatch(1)
inFlight.put(url.toString(), pending!!)
}
}
if (imageUrl.isBlank() && !badUrl) {
val response = executeWithRetries(req)
if (response != null) {
val images = mutableListOf<Pair<Size, String>>()
try {
val json = JSONObject(response.body()?.string())
val imagesJson = json.getJSONObject("album").getJSONArray("image")
for (i in 0 until imagesJson.length()) {
val imageJson = imagesJson.getJSONObject(i)
val size = Size.from(imageJson.optString("size", ""))
val resolvedUrl = imageJson.optString("#text", "")
if (Strings.notEmpty(resolvedUrl)) {
images.add(Pair<Size, String>(size, resolvedUrl))
}
}
} catch (ex: JSONException) {
badUrlCache.put(url.toString(), true)
}
if (images.size > 0) {
/* find the image with the closest to the requested size.
exact match preferred. */
var closest = images[0]
var lastDelta = Integer.MAX_VALUE
for (check in images) {
if (check.first == desiredSize) {
closest = check
break
}
else {
val delta = Math.abs(desiredSize.order - check.first.order)
if (lastDelta > delta) {
closest = check
lastDelta = delta
}
}
}
imageUrl = closest.second
}
}
}
var result: Request? = null
if (imageUrl.isNotBlank()) {
synchronized(urlCache) {
urlCache.put(url.toString(), imageUrl)
}
if (desiredSize == Size.Mega) {
imageUrl = imageUrl.replace("/i/u/300x300", "/i/u/600x600")
}
result = Request.Builder().url(imageUrl).build()
}
synchronized(urlCache) {
if (pending != null) {
pending?.countDown()
inFlight.remove(url.toString())
}
}
return result
}
}

View File

@ -31,16 +31,16 @@ class EmptyListView : FrameLayout {
private var viewOfflineButton: View? = null
private var reconnectButton: View? = null
constructor(context: Context?)
constructor(context: Context)
: super(context) {
initialize()
}
constructor(context: Context?, attrs: AttributeSet?)
constructor(context: Context, attrs: AttributeSet?)
: super(context, attrs) {
initialize()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
: super(context, attrs, defStyleAttr) {
initialize()
}
@ -115,11 +115,11 @@ class EmptyListView : FrameLayout {
addView(mainView)
reconnectButton?.setOnClickListener { _ ->
reconnectButton?.setOnClickListener {
wss.reconnect()
}
viewOfflineButton?.setOnClickListener { _ ->
viewOfflineButton?.setOnClickListener {
val activity = context as Activity
activity.startActivity(TrackListActivity.getOfflineStartIntent(activity))
activity.finish()

View File

@ -33,7 +33,7 @@ class EditPlaylistActivity: BaseActivity() {
mixin(ViewModelMixin(this))
data = mixin(DataProviderMixin())
super.onCreate(savedInstanceState)
playlistName = intent.extras.getString(EXTRA_PLAYLIST_NAME, "-")
playlistName = extras.getString(EXTRA_PLAYLIST_NAME, "-")
title = getString(R.string.playlist_edit_activity, playlistName)
setContentView(R.layout.recycler_view_activity)
viewModel = getViewModel()!!
@ -80,7 +80,8 @@ class EditPlaylistActivity: BaseActivity() {
}
override fun <T: ViewModel<*>> createViewModel(): T? {
return EditPlaylistViewModel(intent.extras.getLong(EXTRA_PLAYLIST_ID, -1L)) as T
@Suppress("unchecked_cast")
return EditPlaylistViewModel(extras.getLong(EXTRA_PLAYLIST_ID, -1L)) as T
}
private fun saveAndFinish() {