package com.github.apognu.otter.activities import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Intent import android.graphics.Bitmap import android.os.Build import android.os.Bundle import android.util.DisplayMetrics import android.view.* import android.view.animation.AccelerateDecelerateInterpolator import android.widget.SeekBar import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toDrawable import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.github.apognu.otter.Otter import com.github.apognu.otter.R import com.github.apognu.otter.fragments.* import com.github.apognu.otter.playback.MediaControlsManager import com.github.apognu.otter.playback.PinService import com.github.apognu.otter.playback.PlayerService import com.github.apognu.otter.repositories.FavoritedRepository import com.github.apognu.otter.repositories.FavoritesRepository import com.github.apognu.otter.repositories.Repository import com.github.apognu.otter.utils.* import com.github.apognu.otter.views.DisableableFrameLayout import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.coroutines.awaitStringResponse import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.offline.DownloadService import com.google.gson.Gson import com.preference.PowerPreference import com.squareup.picasso.Picasso import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.partial_now_playing.* import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class MainActivity : AppCompatActivity() { enum class ResultCode(val code: Int) { LOGOUT(1001) } private val favoriteRepository = FavoritesRepository(this) private val favoritedRepository = FavoritedRepository(this) private var menu: Menu? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) AppContext.init(this) setContentView(R.layout.activity_main) setSupportActionBar(appbar) when (intent.action) { MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment()) } supportFragmentManager .beginTransaction() .replace(R.id.container, BrowseFragment()) .commit() watchEventBus() } override fun onResume() { super.onResume() (container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ -> if (now_playing.isOpened()) { now_playing.close() return@setShouldRegisterTouch false } true } favoritedRepository.update(this, lifecycleScope) startService(Intent(this, PlayerService::class.java)) DownloadService.start(this, PinService::class.java) CommandBus.send(Command.RefreshService) lifecycleScope.launch(IO) { Userinfo.get() } now_playing_toggle.setOnClickListener { CommandBus.send(Command.ToggleState) } now_playing_next.setOnClickListener { CommandBus.send(Command.NextTrack) } now_playing_details_previous.setOnClickListener { CommandBus.send(Command.PreviousTrack) } now_playing_details_next.setOnClickListener { CommandBus.send(Command.NextTrack) } now_playing_details_toggle.setOnClickListener { CommandBus.send(Command.ToggleState) } now_playing_details_progress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onStopTrackingTouch(view: SeekBar?) {} override fun onStartTrackingTouch(view: SeekBar?) {} override fun onProgressChanged(view: SeekBar?, progress: Int, fromUser: Boolean) { if (fromUser) { CommandBus.send(Command.Seek(progress)) } } }) landscape_queue?.let { supportFragmentManager.beginTransaction().replace(R.id.landscape_queue, LandscapeQueueFragment()).commit() } } override fun onBackPressed() { if (now_playing.isOpened()) { now_playing.close() return } super.onBackPressed() } override fun onPrepareOptionsMenu(menu: Menu?): Boolean { this.menu = menu return super.onPrepareOptionsMenu(menu) } override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.toolbar, menu) menu?.findItem(R.id.nav_all_music)?.let { it.isChecked = Settings.getScopes().contains("all") it.isEnabled = !it.isChecked } menu?.findItem(R.id.nav_my_music)?.isChecked = Settings.getScopes().contains("me") menu?.findItem(R.id.nav_followed)?.isChecked = Settings.getScopes().contains("subscribed") return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { now_playing.close() (supportFragmentManager.fragments.last() as? BrowseFragment)?.let { it.selectTabAt(0) return true } launchFragment(BrowseFragment()) } R.id.nav_queue -> launchDialog(QueueFragment()) R.id.nav_search -> startActivity(Intent(this, SearchActivity::class.java)) R.id.nav_all_music, R.id.nav_my_music, R.id.nav_followed -> { menu?.let { menu -> item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) item.actionView = View(this) item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem?) = false override fun onMenuItemActionCollapse(item: MenuItem?) = false }) item.isChecked = !item.isChecked val scopes = Settings.getScopes().toMutableSet() val new = when (item.itemId) { R.id.nav_my_music -> "me" R.id.nav_followed -> "subscribed" else -> { menu.findItem(R.id.nav_all_music).isEnabled = false menu.findItem(R.id.nav_my_music).isChecked = false menu.findItem(R.id.nav_followed).isChecked = false PowerPreference.getDefaultFile().set("scope", "all") EventBus.send(Event.ListingsChanged) return false } } menu.findItem(R.id.nav_all_music).let { it.isChecked = false it.isEnabled = true } scopes.remove("all") when (item.isChecked) { true -> scopes.add(new) false -> scopes.remove(new) } if (scopes.isEmpty()) { menu.findItem(R.id.nav_all_music).let { it.isChecked = true it.isEnabled = false } scopes.add("all") } PowerPreference.getDefaultFile().set("scope", scopes.joinToString(",")) EventBus.send(Event.ListingsChanged) return false } } R.id.nav_downloads -> startActivity(Intent(this, DownloadsActivity::class.java)) R.id.settings -> startActivityForResult(Intent(this, SettingsActivity::class.java), 0) } return true } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == ResultCode.LOGOUT.code) { Intent(this, LoginActivity::class.java).apply { Otter.get().deleteAllData() flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP stopService(Intent(this@MainActivity, PlayerService::class.java)) startActivity(this) finish() } } } private fun launchFragment(fragment: Fragment) { supportFragmentManager.fragments.lastOrNull()?.also { oldFragment -> oldFragment.enterTransition = null oldFragment.exitTransition = null supportFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) } supportFragmentManager .beginTransaction() .setCustomAnimations(0, 0, 0, 0) .replace(R.id.container, fragment) .commit() } private fun launchDialog(fragment: DialogFragment) { supportFragmentManager.beginTransaction().let { fragment.show(it, "") } } @SuppressLint("NewApi") private fun watchEventBus() { lifecycleScope.launch(Main) { EventBus.get().collect { message -> when (message) { is Event.LogOut -> { Otter.get().deleteAllData() startActivity(Intent(this@MainActivity, LoginActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NO_HISTORY }) finish() } is Event.PlaybackError -> toast(message.message) is Event.Buffering -> { when (message.value) { true -> now_playing_buffering.visibility = View.VISIBLE false -> now_playing_buffering.visibility = View.GONE } } is Event.PlaybackStopped -> { if (now_playing.visibility == View.VISIBLE) { (container.layoutParams as? ViewGroup.MarginLayoutParams)?.let { it.bottomMargin = it.bottomMargin / 2 } landscape_queue?.let { landscape_queue -> (landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let { it.bottomMargin = it.bottomMargin / 2 } } now_playing.animate() .alpha(0.0f) .setDuration(400) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animator: Animator?) { now_playing.visibility = View.GONE } }) .start() } } is Event.TrackFinished -> incrementListenCount(message.track) is Event.StateChanged -> { when (message.playing) { true -> { now_playing_toggle.icon = getDrawable(R.drawable.pause) now_playing_details_toggle.icon = getDrawable(R.drawable.pause) } false -> { now_playing_toggle.icon = getDrawable(R.drawable.play) now_playing_details_toggle.icon = getDrawable(R.drawable.play) } } } is Event.QueueChanged -> { findViewById(R.id.nav_queue)?.let { view -> ObjectAnimator.ofFloat(view, View.ROTATION, 0f, 360f).let { it.duration = 500 it.interpolator = AccelerateDecelerateInterpolator() it.start() } } } } } } lifecycleScope.launch(Main) { CommandBus.get().collect { command -> when (command) { is Command.StartService -> { Build.VERSION_CODES.O.onApi( { startForegroundService(Intent(this@MainActivity, PlayerService::class.java).apply { putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString()) }) }, { startService(Intent(this@MainActivity, PlayerService::class.java).apply { putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString()) }) } ) } is Command.RefreshTrack -> refreshCurrentTrack(command.track) is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { AddToPlaylistDialog.show(this@MainActivity, lifecycleScope, command.tracks) } } } } lifecycleScope.launch(Main) { ProgressBus.get().collect { (current, duration, percent) -> now_playing_progress.progress = percent now_playing_details_progress.progress = percent val currentMins = (current / 1000) / 60 val currentSecs = (current / 1000) % 60 val durationMins = duration / 60 val durationSecs = duration % 60 now_playing_details_progress_current.text = "%02d:%02d".format(currentMins, currentSecs) now_playing_details_progress_duration.text = "%02d:%02d".format(durationMins, durationSecs) } } } private fun refreshCurrentTrack(track: Track?) { track?.let { if (now_playing.visibility == View.GONE) { now_playing.visibility = View.VISIBLE now_playing.alpha = 0f now_playing.animate() .alpha(1.0f) .setDuration(400) .setListener(null) .start() (container.layoutParams as? ViewGroup.MarginLayoutParams)?.let { it.bottomMargin = it.bottomMargin * 2 } landscape_queue?.let { landscape_queue -> (landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let { it.bottomMargin = it.bottomMargin * 2 } } } now_playing_title.text = track.title now_playing_album.text = track.artist.name now_playing_toggle.icon = getDrawable(R.drawable.pause) now_playing_details_title.text = track.title now_playing_details_artist.text = track.artist.name now_playing_details_toggle.icon = getDrawable(R.drawable.pause) Picasso.get() .maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original)) .fit() .centerCrop() .into(now_playing_cover) now_playing_details_cover?.let { now_playing_details_cover -> Picasso.get() .maybeLoad(maybeNormalizeUrl(track.album?.cover())) .fit() .centerCrop() .transform(RoundedCornersTransformation(16, 0)) .into(now_playing_details_cover) } if (now_playing_details_cover == null) { lifecycleScope.launch(Default) { val width = DisplayMetrics().apply { windowManager.defaultDisplay.getMetrics(this) }.widthPixels val backgroundCover = Picasso.get() .maybeLoad(maybeNormalizeUrl(track.album?.cover())) .get() .run { Bitmap.createScaledBitmap(this, width, width, false).toDrawable(resources) } .apply { alpha = 20 gravity = Gravity.CENTER } withContext(Main) { now_playing_details.background = backgroundCover } } } now_playing_details_repeat?.let { now_playing_details_repeat -> changeRepeatMode(Cache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0) now_playing_details_repeat.setOnClickListener { val current = Cache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0 changeRepeatMode((current + 1) % 3) } } now_playing_details_info?.let { now_playing_details_info -> now_playing_details_info.setOnClickListener { PopupMenu(this@MainActivity, now_playing_details_info, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply { inflate(R.menu.track_info) setOnMenuItemClickListener { when (it.itemId) { R.id.track_info_artist -> ArtistsFragment.openAlbums(this@MainActivity, track.artist, art = track.album?.cover()) R.id.track_info_album -> AlbumsFragment.openTracks(this@MainActivity, track.album) R.id.track_info_details -> TrackInfoDetailsFragment.new(track).show(supportFragmentManager, "dialog") } now_playing.close() true } show() } } } now_playing_details_favorite?.let { now_playing_details_favorite -> favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ -> lifecycleScope.launch(Main) { track.favorite = favorites.contains(track.id) when (track.favorite) { true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite)) false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground)) } } } now_playing_details_favorite.setOnClickListener { when (track.favorite) { true -> { favoriteRepository.deleteFavorite(track.id) now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground)) } false -> { favoriteRepository.addFavorite(track.id) now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite)) } } track.favorite = !track.favorite favoriteRepository.fetch(Repository.Origin.Network.origin) } now_playing_details_add_to_playlist.setOnClickListener { CommandBus.send(Command.AddToPlaylist(listOf(track))) } } } } private fun changeRepeatMode(index: Int) { when (index) { // From no repeat to repeat all 0 -> { Cache.set(this@MainActivity, "repeat", "0".toByteArray()) now_playing_details_repeat?.setImageResource(R.drawable.repeat) now_playing_details_repeat?.setColorFilter(ContextCompat.getColor(this, R.color.controlForeground)) now_playing_details_repeat?.alpha = 0.2f CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_OFF)) } // From repeat all to repeat one 1 -> { Cache.set(this@MainActivity, "repeat", "1".toByteArray()) now_playing_details_repeat?.setImageResource(R.drawable.repeat) now_playing_details_repeat?.setColorFilter(ContextCompat.getColor(this, R.color.controlForeground)) now_playing_details_repeat?.alpha = 1.0f CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ALL)) } // From repeat one to no repeat 2 -> { Cache.set(this@MainActivity, "repeat", "2".toByteArray()) now_playing_details_repeat?.setImageResource(R.drawable.repeat_one) now_playing_details_repeat?.setColorFilter(ContextCompat.getColor(this, R.color.controlForeground)) now_playing_details_repeat?.alpha = 1.0f CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ONE)) } } } private fun incrementListenCount(track: Track?) { track?.let { lifecycleScope.launch(IO) { try { Fuel .post(mustNormalizeUrl("/api/v1/history/listenings/")) .authorize() .header("Content-Type", "application/json") .body(Gson().toJson(mapOf("track" to track.id))) .awaitStringResponse() } catch (_: Exception) { } } } } }