Merge branch 'tsumugi/master' into tsumugi/release

remotes/origin/devel
yattoz 2020-01-12 02:26:25 +01:00
commit ae1d1a1e44
27 changed files with 446 additions and 86 deletions

View File

@ -29,8 +29,8 @@ android {
applicationId "fr.forum_thalie.tsumugi"
minSdkVersion 16
targetSdkVersion 29
versionCode 100
versionName "1.0.0"
versionCode 110
versionName "1.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}

View File

@ -31,9 +31,6 @@
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<service android:name=".streamerNotificationService.StreamerMonitorService"
android:enabled="true"
android:exported="true"/>
<receiver android:name=".BootBroadcastReceiver"
android:directBootAware="true"

View File

@ -13,7 +13,7 @@ class Async(val handler: (Any?) -> Any?, val post: (Any?) -> Unit = {},
execute()
} catch (e: Exception)
{
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag,e.toString())
//[REMOVE LOG CALLS]Log.d(tag,e.toString())
}
}
@ -46,14 +46,14 @@ class Async(val handler: (Any?) -> Any?, val post: (Any?) -> Unit = {},
}
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "fallback for no network. Store reset : $storeReset")
//[REMOVE LOG CALLS]Log.d(tag, "fallback for no network. Store reset : $storeReset")
}
override fun doInBackground(vararg params: Any?): Any? {
try {
return handler(parameters)
} catch (e: Exception) {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag,e.toString())
//[REMOVE LOG CALLS]Log.d(tag,e.toString())
onException(e)
}
return null
@ -63,7 +63,7 @@ class Async(val handler: (Any?) -> Any?, val post: (Any?) -> Unit = {},
try {
post(result)
} catch (e: Exception) {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag,e.toString())
//[REMOVE LOG CALLS]Log.d(tag,e.toString())
onException(e)
}
}

View File

@ -21,7 +21,7 @@ abstract class BaseActivity : AppCompatActivity() {
val height = ((rootLayout?.height ?: 0))
val width = ((rootLayout?.width ?: 0))
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$viewWidth, $viewHeight, $width, $height, ${viewHeight.toDouble()/viewWidth.toDouble()}, ${height.toDouble()/width.toDouble()}")
//[REMOVE LOG CALLS]Log.d(tag, "$viewWidth, $viewHeight, $width, $height, ${viewHeight.toDouble()/viewWidth.toDouble()}, ${height.toDouble()/width.toDouble()}")
val broadcastManager = LocalBroadcastManager.getInstance(this@BaseActivity)
if(height <= viewHeight * 2 / 3 /*height.toDouble()/width.toDouble() < 1.20 */){
@ -49,14 +49,14 @@ abstract class BaseActivity : AppCompatActivity() {
// do things when keyboard is shown
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.visibility = View.GONE
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "bottomNav visibility set to GONE (height $keyboardHeight)")
//[REMOVE LOG CALLS]Log.d(tag, "bottomNav visibility set to GONE (height $keyboardHeight)")
}
private fun onHideKeyboard() {
// do things when keyboard is hidden
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.visibility = View.VISIBLE
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "bottomNav visibility set to VISIBLE")
//[REMOVE LOG CALLS]Log.d(tag, "bottomNav visibility set to VISIBLE")
}
protected fun attachKeyboardListeners() {

View File

@ -12,7 +12,7 @@ import fr.forum_thalie.tsumugi.playerstore.PlayerStore
class BootBroadcastReceiver : BroadcastReceiver(){
override fun onReceive(context: Context, arg1: Intent) {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "Broadcast Receiver received $arg1")
//[REMOVE LOG CALLS]Log.d(tag, "Broadcast Receiver received $arg1")
// define preferenceStore for places of the program that needs to access Preferences without a context
preferenceStore = PreferenceManager.getDefaultSharedPreferences(context)
@ -22,7 +22,10 @@ class BootBroadcastReceiver : BroadcastReceiver(){
if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
{
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
if (!PlayerStore.instance.isInitialized)
PlayerStore.instance.initApi()
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
PlayerStore.instance.initPicture(context)

View File

@ -129,10 +129,16 @@ class MainActivity : BaseActivity() {
colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
// fetch program
Planning.instance.parseUrl(/* getString(R.string.planning_url) */ context = this)
PlayerStore.instance.initUrl(this)
PlayerStore.instance.initApi()
// Post-UI Launch
if (PlayerStore.instance.isInitialized)
{
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "skipped initialization")
//[REMOVE LOG CALLS]Log.d(tag, "skipped initialization")
} else {
// if the service is not started, start it in STOP mode.
// It's not a dummy action : with STOP mode, the player does not buffer audio (and does not use data connection without the user's consent).
@ -160,9 +166,6 @@ class MainActivity : BaseActivity() {
isTimerStarted = true
}
// fetch program
Planning.instance.parseUrl(/* getString(R.string.planning_url) */ context = this)
// initialize the UI
setTheme(R.style.AppTheme)
setContentView(R.layout.activity_main)
@ -193,7 +196,7 @@ class MainActivity : BaseActivity() {
val i = Intent(this, RadioService::class.java)
i.putExtra("action", a.name)
i.putExtra("value", v)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "Sending intent ${a.name}")
//[REMOVE LOG CALLS]Log.d(tag, "Sending intent ${a.name}")
startService(i)
}
@ -238,7 +241,7 @@ class MainActivity : BaseActivity() {
// File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/MyPersonalAppFolder")
val logDirectory = File("$appDirectory/log")
val logFile = File(logDirectory, "logcat" + System.currentTimeMillis() + ".txt")
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(
//[REMOVE LOG CALLS]Log.d(
tag,
"appDirectory : $appDirectory, logDirectory : $logDirectory, logFile : $logFile"
)
@ -246,20 +249,20 @@ class MainActivity : BaseActivity() {
// create app folder
if (!appDirectory.exists()) {
appDirectory.mkdir()
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$appDirectory created")
//[REMOVE LOG CALLS]Log.d(tag, "$appDirectory created")
}
// create log folder
if (!logDirectory.exists()) {
logDirectory.mkdir()
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$logDirectory created")
//[REMOVE LOG CALLS]Log.d(tag, "$logDirectory created")
}
// clear the previous logcat and then write the new one to the file
try {
Runtime.getRuntime().exec("logcat -c")
Runtime.getRuntime().exec("logcat -v time -f $logFile *:E $tag:V ")
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "logcat started")
//[REMOVE LOG CALLS]Log.d(tag, "logcat started")
} catch (e: IOException) {
e.printStackTrace()
}

View File

@ -1,6 +1,7 @@
package fr.forum_thalie.tsumugi
import android.os.Bundle
import android.view.MenuItem
import fr.forum_thalie.tsumugi.preferences.*
@ -38,4 +39,15 @@ class ParametersActivity : BaseActivity() {
.replace(R.id.parameters_host_container, fragmentToLoad)
.commit()
}
// Make the Up button function as back instead of always bringing us to the main activity
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
}

View File

@ -68,14 +68,14 @@ class RadioService : MediaBrowserServiceCompat() {
// This *should* work in any case...
when (intent.getIntExtra("state", -1)) {
0 -> {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is unplugged")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is unplugged")
}
1 -> {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is plugged")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is plugged")
headsetPluggedIn = true
}
else -> {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "I have no idea what the headset state is")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "I have no idea what the headset state is")
}
}
/*
@ -91,7 +91,7 @@ class RadioService : MediaBrowserServiceCompat() {
}
else
{
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Can't get state?")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Can't get state?")
}
*/
@ -124,10 +124,32 @@ class RadioService : MediaBrowserServiceCompat() {
private val titleObserver = Observer<String> {
// We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
{
//[REMOVE LOG CALLS]Log.d((tag, radioTag + "SONG CHANGED AND PLAYING")
// we activate latency compensation only if it's been at least 2 songs...
when {
PlayerStore.instance.isStreamDown -> {
// if we reach here, it means that the observer has been called by a new song and that the stream was down previously.
// so the stream is now back to normal.
PlayerStore.instance.isStreamDown = false
PlayerStore.instance.initApi()
}
PlayerStore.instance.currentSong.title.value == noConnectionValue -> {
PlayerStore.instance.isStreamDown = true
}
else -> {
PlayerStore.instance.fetchApi(numberOfSongs >= 2)
}
}
}
if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
&& it != noConnectionValue)
{
PlayerStore.instance.updateLp()
PlayerStore.instance.updateQueue()
}
nowPlayingNotification.update(this)
Planning.instance.checkProgramme()
@ -163,6 +185,12 @@ class RadioService : MediaBrowserServiceCompat() {
preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
// start ticker for when the player is stopped
val periodString = PreferenceManager.getDefaultSharedPreferences(this).getString("fetchPeriod", "10") ?: "10"
val period: Long = Integer.parseInt(periodString).toLong()
if (period > 0)
apiTicker.schedule(ApiFetchTick(), 0, period * 1000)
// Define managers
telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
@ -205,7 +233,7 @@ class RadioService : MediaBrowserServiceCompat() {
startForeground(radioServiceId, nowPlayingNotification.notification)
PlayerStore.instance.isServiceStarted.value = true
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "created")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "created")
}
private val handler = Handler()
@ -246,7 +274,7 @@ class RadioService : MediaBrowserServiceCompat() {
Actions.CANCEL_FADE_OUT.name -> { handler.removeCallbacks(lowerVolumeRunnable) }
Actions.SNOOZE.name -> { RadioAlarm.instance.snooze(this) }
}
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "intent received : " + intent.getStringExtra("action"))
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "intent received : " + intent.getStringExtra("action"))
super.onStartCommand(intent, flags, startId)
// The service must be re-created if it is destroyed by the system. This allows the user to keep actions like Bluetooth and headphones plug available.
return START_STICKY
@ -258,7 +286,7 @@ class RadioService : MediaBrowserServiceCompat() {
stopSelf()
}
super.onTaskRemoved(rootIntent)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "task removed")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "task removed")
}
override fun onDestroy() {
@ -290,7 +318,7 @@ class RadioService : MediaBrowserServiceCompat() {
}
apiTicker.cancel() // stops the timer.
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "destroyed")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "destroyed")
// if the service is destroyed, the application had become useless.
exitProcess(0)
}
@ -361,10 +389,10 @@ class RadioService : MediaBrowserServiceCompat() {
for (i in 0 until it.length()) {
val entry = it.get(i)
if (entry is IcyHeaders) {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
}
if (entry is IcyInfo) {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: Title ----> ${entry.title}")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: Title ----> ${entry.title}")
// Note : Kotlin supports UTF-8 by default.
numberOfSongs++
val data = entry.title!!
@ -439,7 +467,7 @@ class RadioService : MediaBrowserServiceCompat() {
{
Thread.sleep(1000)
i++
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$i, isAlarmStopped=$isAlarmStopped")
//[REMOVE LOG CALLS]Log.d(tag, "$i, isAlarmStopped=$isAlarmStopped")
}
}
val post: (Any?) -> Unit = {
@ -512,7 +540,7 @@ class RadioService : MediaBrowserServiceCompat() {
SystemClock.elapsedRealtime()
)
mediaSession.setPlaybackState(playbackStateBuilder.build())
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "begin playing")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "begin playing")
}
private fun pausePlaying()
@ -541,7 +569,7 @@ class RadioService : MediaBrowserServiceCompat() {
1.0f,
SystemClock.elapsedRealtime()
)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "stopped")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "stopped")
mediaSession.setPlaybackState(playbackStateBuilder.build())
}
@ -629,7 +657,7 @@ class RadioService : MediaBrowserServiceCompat() {
Player.STATE_ENDED -> state = "Player.STATE_ENDED"
Player.STATE_READY -> state = "Player.STATE_READY"
}
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
}
}

View File

@ -1,8 +1,18 @@
package fr.forum_thalie.tsumugi
import android.support.v4.media.session.PlaybackStateCompat
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
import java.util.*
class ApiFetchTick : TimerTask() {
override fun run() {
if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
{
PlayerStore.instance.fetchApi()
}
}
}
class Tick : TimerTask() {
override fun run() {
PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)

View File

@ -97,7 +97,7 @@ class RadioAlarm {
calendar.isLenient = true
calendar.set(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH) + i, hourOfDay, minute)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, calendar.toString())
//[REMOVE LOG CALLS]Log.d(tag, calendar.toString())
return calendar.timeInMillis

View File

@ -61,7 +61,7 @@ class RadioSleeper {
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, currentMillis + (minutes * 60 * 1000) - (1 * 60 * 1000), fadeOutIntent)
sleepAtMillis.value = System.currentTimeMillis() + (minutes * 60 * 1000) - 1 // this -1 allows to round the division for display at the right integer
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "set sleep to $minutes minutes")
//[REMOVE LOG CALLS]Log.d(tag, "set sleep to $minutes minutes")
}
}
@ -79,7 +79,7 @@ class RadioSleeper {
c.startService(cancelFadeOutIntent)
}
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "cancelled sleep")
//[REMOVE LOG CALLS]Log.d(tag, "cancelled sleep")
sleepAtMillis.value = null
}
}

View File

@ -35,7 +35,7 @@ class Programme (val title: String, private val periodicity: Int, private val ho
val isSpanningOverNight =
(((0b1000000 shr ((currentDay - 1) % 7) and (periodicity)) != 0) && hourEnd < hourBegin)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$title is today: $isToday or spanning $isSpanningOverNight")
//[REMOVE LOG CALLS]Log.d(tag, "$title is today: $isToday or spanning $isSpanningOverNight")
// shr = shift-right. It's a binary mask.
// if the program started yesterday, and spanned over night, it means that there could be a chance that it's still active.
@ -87,6 +87,6 @@ class Programme (val title: String, private val periodicity: Int, private val ho
*/
init {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, this.toString())
//[REMOVE LOG CALLS]Log.d(tag, this.toString())
}
}

View File

@ -7,9 +7,16 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.lifecycle.MutableLiveData
import fr.forum_thalie.tsumugi.*
import org.json.JSONObject
import java.net.URL
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
class PlayerStore {
private lateinit var urlToScrape: String
val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
val volume: MutableLiveData<Int> = MutableLiveData()
@ -27,6 +34,7 @@ class PlayerStore {
val listenersCount: MutableLiveData<Int> = MutableLiveData()
var latencyCompensator : Long = 0
var isInitialized: Boolean = false
var isStreamDown: Boolean = false
init {
playbackState.value = PlaybackStateCompat.STATE_STOPPED
@ -43,10 +51,131 @@ class PlayerStore {
listenersCount.value = 0
}
fun initUrl(c: Context)
{
urlToScrape = c.getString(R.string.API_URL)
}
private fun getTimestamp(s: String) : Long
{
val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
try {
val t: Date? = dateFormat.parse(s)
return t!!.time
} catch (e: ParseException) {
e.printStackTrace()
}
return 0
}
// ##################################################
// ################# API FUNCTIONS ##################
// ##################################################
private fun updateApi(res: JSONObject, isCompensatingLatency : Boolean = false) {
// If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that.
val resMain = res.getJSONObject("tracks").getJSONObject("current")
val s = extractSong(resMain)
if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
|| currentSong.title.value == noConnectionValue)
currentSong.setTitleArtist("${s.artist.value} - ${s.title.value}")
val starts = s.startTime.value
val ends = s.stopTime.value
if (currentSong.startTime.value != starts)
currentSong.startTime.value = starts
currentSong.stopTime.value = ends
// I noticed that the server has a big (3 to 9 seconds !!) offset for current time.
// we can measure it when the player is playing, to compensate it and have our progress bar perfectly timed
// latencyCompensator is set to null when beginPlaying() (we can't measure it at the moment we start playing, since we're in the middle of a song),
// at this moment, we set it to 0. Then, next time the updateApi is called when we're playing, we measure the latency and we set out latencyComparator.
if(isCompensatingLatency)
{
latencyCompensator = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (currentSong.startTime.value ?: getTimestamp(res.getJSONObject("station").getString("schedulerTime")))
//[REMOVE LOG CALLS]Log.d((tag, "latency compensator set to ${(latencyCompensator).toFloat()/1000} s")
}
currentTime.value = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (latencyCompensator)
/*
val listeners = resMain.getInt("listeners")
listenersCount.value = listeners
//[REMOVE LOG CALLS]Log.d((tag, playerStoreTag + "store updated")
*/
}
private val scrape : (Any?) -> String =
{
URL(urlToScrape).readText()
}
/* initApi is called :
- at startup
- when a streamer changes.
the idea is to fetch the queue when a streamer changes (potentially Hanyuu), and at startup.
The Last Played is only fetched if it's empty (so, only at startup), not when a streamer changes.
*/
fun initApi()
{
val post : (parameter: Any?) -> Unit = {
val result = JSONObject(it as String)
if (result.has("tracks"))
{
updateApi(result)
currentSongBackup.copy(currentSong)
isQueueUpdated.value = true
isLpUpdated.value = true
}
isInitialized = true
}
Async(scrape, post)
}
fun fetchApi(isCompensatingLatency: Boolean = false) {
val post: (parameter: Any?) -> Unit = {
val result = JSONObject(it as String)
if (!result.isNull("tracks"))
{
updateApi(result, isCompensatingLatency)
}
}
Async(scrape, post)
}
private fun extractSong(songJSON: JSONObject) : Song {
val song = Song()
song.setTitleArtist(songJSON.getString("name"))
song.startTime.value = getTimestamp(songJSON.getString("starts"))
song.stopTime.value = getTimestamp(songJSON.getString("ends"))
song.type.value = 0 // only used for R/a/dio
return song
}
// ##################################################
// ############## QUEUE / LP FUNCTIONS ##############
// ##################################################
fun updateQueue() {
if (queue.isNotEmpty()) {
queue.remove(queue.first())
//[REMOVE LOG CALLS]Log.d((tag, queue.toString())
fetchLastRequest()
isQueueUpdated.value = true
} else if (isInitialized) {
fetchLastRequest()
} else {
//[REMOVE LOG CALLS]Log.d((tag, "queue is empty! fetching anyway !!")
fetchLastRequest()
}
}
fun updateLp() {
// note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
//if (lp.isNotEmpty()){
@ -56,11 +185,79 @@ class PlayerStore {
lp.add(0, n)
currentSongBackup.copy(currentSong)
isLpUpdated.value = true
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + lp.toString())
//[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + lp.toString())
//}
}
private fun fetchLastRequest()
{
val sleepScrape: (Any?) -> String = {
/* we can maximize our chances to retrieve the last queued song by specifically waiting for the number of seconds we measure between ICY metadata and API change.
we add 2 seconds just to get a higher probability that the API has correctly updated. (the latency compensator can have a jitter of 1 second usually)
If, against all odds, the API hasn't updated yet, we will retry in the same amount of seconds. So we'll have the data anyway.
This way to fetch at the most probable time is a good compromise between fetch speed and fetch frequency
We don't fetch too often, and we start to fetch at the most *probable* time.
If there's no latencyCompensator measured yet, we only wait for 3 seconds.
If the song is the same, it will be called again. 3 seconds is a good compromise between speed and frequency:
it might be called twice, rarely 3 times, and it's only the 2 first songs ; after these, the latencyCompensator is set to fetch at the most probable time.
*/
val sleepTime: Long = if (latencyCompensator > 0) latencyCompensator + 2000 else 3000
Thread.sleep(sleepTime) // we wait a bit (10s) for the API to get updated on R/a/dio side!
URL(urlToScrape).readText()
}
lateinit var post: (parameter: Any?) -> Unit
fun postFun(result: JSONObject)
{
if (result.has("tracks")) {
val resMain = result.getJSONObject("tracks")
/*
if ((resMain.has("isafkstream") && !resMain.getBoolean("isafkstream")) &&
queue.isNotEmpty())
{
queue.clear() //we're not requesting anything anymore.
isQueueUpdated.value = true
} else if (resMain.has("isafkstream") && resMain.getBoolean("isafkstream") &&
queue.isEmpty())
{
initApi()
} else
*/
if (resMain.has("next") /*&& queue.isNotEmpty()*/) {
val queueJSON =
resMain.getJSONObject("next")
val t = extractSong(queueJSON)
if (queue.isNotEmpty() && t == queue.last())
{
//[REMOVE LOG CALLS]Log.d((tag, playerStoreTag + "Song already in there: $t")
Async(sleepScrape, post)
} else {
queue.add(queue.size, t)
//[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + "added last queue song: $t")
isQueueUpdated.value = true
}
}
}
}
post = {
val result = JSONObject(it as String)
/* The goal is to pass the result to a function that will process it (postFun).
The magic trick is, under circumstances, the last queue song might not have been updated yet when we fetch it.
So if this is detected ==> if (t == queue.last() )
Then the function re-schedule an Async(sleepScrape, post).
To do that, the "post" must be defined BEFORE the function, but the function must be defined BEFORE the "post" value.
So I declare "post" as lateinit var, define the function, then define the "post" that calls the function. IT SHOULD WORK.
*/
postFun(result)
}
Async(sleepScrape, post)
}
// ##################################################
// ############## PICTURE FUNCTIONS #################
// ##################################################

View File

@ -1,9 +1,6 @@
package fr.forum_thalie.tsumugi.preferences
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.preference.*
import fr.forum_thalie.tsumugi.R
@ -40,5 +37,25 @@ class CustomizeFragment : PreferenceFragmentCompat() {
true
}
val fetchPeriod = preferenceScreen.findPreference<ListPreference>("fetchPeriod")
fetchPeriod?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
fetchPeriod?.setOnPreferenceChangeListener { _, newValue ->
val builder1 = AlertDialog.Builder(context!!)
if (Integer.parseInt(newValue as String) == 0)
builder1.setMessage(R.string.restart_the_app)
else
builder1.setMessage(R.string.restart_the_app)
builder1.setCancelable(true)
builder1.setPositiveButton("Close" ) { dialog, _ ->
dialog.cancel()
}
val alert11 = builder1.create()
alert11.show()
true
}
}
}

View File

@ -43,9 +43,9 @@ class NewsFragment : Fragment() {
}
newsViewModel.isWebViewLoaded = true
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "webview created")
//[REMOVE LOG CALLS]Log.d(tag, "webview created")
} else {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "webview already created!?")
//[REMOVE LOG CALLS]Log.d(tag, "webview already created!?")
}
newsViewModel.root.addOnLayoutChangeListener(orientationLayoutListener)
@ -101,7 +101,7 @@ class NewsFragment : Fragment() {
ViewModelProviders.of(this).get(NewsViewModel::class.java)
newsViewModel.fetch(c = context!!, isPreloading = true)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "news fetched onCreate")
//[REMOVE LOG CALLS]Log.d(tag, "news fetched onCreate")
super.onCreate(savedInstanceState)
}
}

View File

@ -43,14 +43,14 @@ class NewsViewModel : ViewModel() {
val maxNumberOfArticles = 5
coroutineScope.launch(Dispatchers.Main) {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "launching coroutine")
//[REMOVE LOG CALLS]Log.d(tag, "launching coroutine")
val parser = Parser()
try {
val articleList = parser.getArticles(urlToScrape)
newsArray.clear()
for (i in 0 until min(articleList.size, maxNumberOfArticles)) {
val item = articleList[i]
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "i = $i / ${articleList.size}")
//[REMOVE LOG CALLS]Log.d(tag, "i = $i / ${articleList.size}")
val news = News()
news.title = item.title ?: ""
news.link = item.link ?: urlToScrape
@ -60,7 +60,7 @@ class NewsViewModel : ViewModel() {
val formatter6 = SimpleDateFormat(newsDateTimePattern, Locale.ENGLISH)
val dateString = item.pubDate.toString()
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$news --- $dateString")
//[REMOVE LOG CALLS]Log.d(tag, "$news --- $dateString")
news.date = formatter6.parse(dateString) ?: Date(0)

View File

@ -49,12 +49,12 @@ class NowPlayingFragment : Fragment() {
val volumeText: TextView = root.findViewById(R.id.volume_text)
val progressBar: ProgressBar = root.findViewById(R.id.progressBar)
val volumeIconImage : ImageView = root.findViewById(R.id.volume_icon)
val currentProgrammeText: TextView = root.findViewById(R.id.text_current_programme)
val streamerPictureImageView: ImageView = root.findViewById(R.id.streamerPicture)
// Note: these values are not used in the generic app, but if you want to, you can use them.
val songTitleNextText: TextView = root.findViewById(R.id.text_song_title_next)
//val songArtistNextText: TextView = root.findViewById(R.id.text_song_artist_next)
val songArtistNextText: TextView = root.findViewById(R.id.text_song_artist_next)
/*
val streamerNameText : TextView = root.findViewById(R.id.streamerName)
@ -69,15 +69,15 @@ class NowPlayingFragment : Fragment() {
listenersText,8, 16, 2, TypedValue.COMPLEX_UNIT_SP)
*/
/*
// trick : I can't observe the queue because it's an ArrayDeque that doesn't trigger any change...
// so I observe a dedicated Mutable that gets set when the queue is updated.
PlayerStore.instance.isQueueUpdated.observe(viewLifecycleOwner, Observer {
val t = if (PlayerStore.instance.queue.size > 0) PlayerStore.instance.queue[0] else Song("No queue - ") // (it.peekFirst != null ? it.peekFirst : Song() )
val t = if (PlayerStore.instance.queue.size > 0) PlayerStore.instance.queue[0] else Song(noConnectionValue) // (it.peekFirst != null ? it.peekFirst : Song() )
songTitleNextText.text = t.title.value
songArtistNextText.text = t.artist.value
})
/*
PlayerStore.instance.streamerName.observe(viewLifecycleOwner, Observer {
streamerNameText.text = it
})
@ -92,10 +92,12 @@ class NowPlayingFragment : Fragment() {
songTitleText.text = it
})
Planning.instance.currentProgramme.observe(viewLifecycleOwner, Observer {
songTitleNextText.text = it
currentProgrammeText.text = it
})
PlayerStore.instance.currentSong.artist.observe(viewLifecycleOwner, Observer {
songArtistText.text = it
})
@ -273,7 +275,7 @@ class NowPlayingFragment : Fragment() {
(viewHeight*100)/viewWidth
else
100
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "orientation set")
//[REMOVE LOG CALLS]Log.d(tag, "orientation set")
}
override fun onResume() {

View File

@ -37,11 +37,11 @@ class ProgrammeFragment : Fragment() {
viewPager.adapter = adapter
val todaySundayFirst = Calendar.getInstance(Planning.instance.timeZone).get(Calendar.DAY_OF_WEEK) - 1
viewPager.currentItem = (todaySundayFirst - 1)%7
viewPager.currentItem = (todaySundayFirst - 1 + 7)%7 // don't do modulos on negative, seems like it's weird
val tabLayout : TabLayout = root.findViewById(R.id.dayTabLayout)
tabLayout.setupWithViewPager(viewPager)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "SongFragment view created")
//[REMOVE LOG CALLS]Log.d(tag, "SongFragment view created")
return root
}

View File

@ -39,7 +39,7 @@ class SongsFragment : Fragment() {
val tabLayout : TabLayout = root.findViewById(R.id.tabLayout)
tabLayout.setupWithViewPager(viewPager)
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "SongFragment view created")
//[REMOVE LOG CALLS]Log.d(tag, "SongFragment view created")
return root
}

View File

@ -31,7 +31,7 @@ class LastPlayedFragment : Fragment() {
private val queueObserver = Observer<Boolean> {
//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, lastPlayedFragmentTag + "queue changed")
//[REMOVE LOG CALLS]Log.d(tag, lastPlayedFragmentTag + "queue changed")
viewAdapter.notifyDataSetChanged()
}

View File

@ -93,13 +93,6 @@
android:visibility="gone"
/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/topInfoGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.95" />
<TextView
android:id="@+id/sleepInfo"
android:layout_width="0dp"
@ -162,7 +155,8 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="@id/topInfoGuideline"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toTopOf="@id/text_current_programme"
app:layout_constraintEnd_toStartOf="@id/streamerPicture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seek_bar_volume">
@ -184,7 +178,7 @@
android:layout_gravity="top"
android:text="@string/up_next"
android:textAlignment="center"
android:textColor="@color/whited3"
android:textColor="@color/whited5"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -199,10 +193,10 @@
android:text=""
android:textAlignment="center"
android:textColor="@color/whited3"
android:textSize="16sp"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/upNext"
android:visibility="gone"/>
android:visibility="visible"/>
<TextView
android:id="@+id/text_song_title_next"
@ -212,14 +206,50 @@
android:text=""
android:textAlignment="center"
android:textColor="@color/whited"
android:textSize="16sp"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_song_artist_next" />
app:layout_constraintTop_toBottomOf="@id/text_song_artist_next"
android:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<TextView
android:id="@+id/current_programme"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:text="@string/current_programme"
android:textAlignment="center"
android:textColor="@color/whited3"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="@id/topInfoGuideline"
android:visibility="visible" />
<TextView
android:id="@+id/text_current_programme"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text=""
android:layout_marginStart="8sp"
android:layout_marginLeft="8sp"
android:gravity="start|bottom"
android:textAlignment="textStart"
android:textColor="@color/whited"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/current_programme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/topInfoGuideline"
android:visibility="visible"
/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/topInfoGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.97" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -268,7 +298,7 @@
android:text=""
android:textAlignment="center"
android:textColor="@color/whited"
android:textSize="20sp"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
@ -280,7 +310,7 @@
android:text=""
android:textAlignment="center"
android:textColor="@color/whited3"
android:textSize="20sp"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/text_song_title"
app:layout_constraintStart_toStartOf="parent" />
@ -305,7 +335,7 @@
android:progressDrawable="@drawable/progress_bar_progress"
app:layout_constraintBottom_toTopOf="@id/play_pause"
tools:layout_editor_absoluteX="0dp"
android:visibility="gone"/>
android:visibility="visible"/>
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
<TextView
@ -318,7 +348,7 @@
android:textAlignment="textEnd"
app:layout_constraintEnd_toEndOf="@id/progressBar"
app:layout_constraintTop_toBottomOf="@id/progressBar"
android:visibility="gone"/>
android:visibility="visible"/>
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
<TextView
@ -331,7 +361,7 @@
android:textAlignment="textStart"
app:layout_constraintStart_toStartOf="@id/progressBar"
app:layout_constraintTop_toBottomOf="@id/progressBar"
android:visibility="gone"/>
android:visibility="visible"/>
@ -381,7 +411,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.33" />
app:layout_constraintGuide_percent="0.38" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/splitHorizontalLayout"

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="snoozeValues">
<item name="0">@string/disable</item>
<item name="1">1</item>
<item name="2">2</item>
<item name="5">5</item>
<item name="10">10</item>
<item name="15">15</item>
<item name="20">20</item>
<item name="25">25</item>
<item name="30">30</item>
</string-array>
<string-array name="fetchPeriodString">
<item name="5">Toutes les 5 secondes (mise à jour rapide)</item>
<item name="10">Toutes les 10 secondes</item>
<item name="15">Toutes les 15 secondes</item>
<item name="20">Toutes les 20 secondes</item>
<item name="30">Toutes les 30 secondes (moins d\'utilisation de batterie)</item>
</string-array>
<string-array name="fetchPeriodValues">
<item name="5">5</item>
<item name="10">10</item>
<item name="15">15</item>
<item name="20">20</item>
<item name="30">30</item>
</string-array>
</resources>

View File

@ -10,7 +10,8 @@
<string name="volume">Volume : </string>
<string name="up_next">Émission en cours :</string>
<string name="up_next">Prochain titre :</string>
<string name="current_programme">Émission en cours : </string>
<string name="now_streaming">En cours</string>
<string name="error_webView">Erreur du chargement de WebView. Téléchargez Google Chrome sur le Play Store, ou activez le si vous l\'avez désactivé.</string>
<string name="action_settings">Paramètres</string>
@ -50,5 +51,6 @@
<string name="sleepClosesApp">Minuterie avant fermeture de l\'application</string>
<string name="setSleepDuration">Choisir une durée (en minutes)</string>
<string name="willCloseIn">Extinction dans %1$d minutes</string>
<string name="fetchPeriod">Choisir la fréquence de mise à jour quand la radio est stoppée</string>
</resources>

View File

@ -13,4 +13,19 @@
<item name="30">30</item>
</string-array>
<string-array name="fetchPeriodString">
<item name="5">Every 5 seconds (faster update)</item>
<item name="10">Every 10 seconds</item>
<item name="15">Every 15 seconds</item>
<item name="20">Every 20 seconds</item>
<item name="30">Every 30 seconds (less battery-intensive)</item>
</string-array>
<string-array name="fetchPeriodValues">
<item name="5">5</item>
<item name="10">10</item>
<item name="15">15</item>
<item name="20">20</item>
<item name="30">30</item>
</string-array>
</resources>

View File

@ -11,7 +11,7 @@
<color name="whited3">#999999</color>
<color name="whited4">#999999</color>
<color name="whited5">#7F7F7F</color>
<color name="rblue">#4FC3F7</color>
<color name="rblue">#64B5F6</color>
<color name="bluereq">#527a8e</color>
<color name="blueTitle">#527a8e</color>
<color name="reqButtonPressed">#5cb85c</color>
@ -20,10 +20,10 @@
<color name="seek_bar_background">#585858</color>
<color name="seek_bar_progress">#f58b01</color>
<color name="seek_bar_secondary_progress">#4FC3F7</color>
<color name="seek_bar_secondary_progress">#64B5F6</color>
<color name="progress_bar_background">#585858</color>
<color name="progress_bar_progress">#4FC3F7</color>
<color name="progress_bar_progress">#64B5F6</color>
<color name="progress_bar_secondary_progress">#f58b01</color>
<color name="white">#FFFFFF</color>

View File

@ -7,6 +7,7 @@
<string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
<string name="website_url">https://tsumugi.forum-thalie.fr/</string>
<string name="rss_url">https://tsumugi.forum-thalie.fr/?feed=rss2</string>
<string name="API_URL">https://radio.mahoro-net.org/airtime/api/live-info-v2</string>
<string name="planning_url">ADD SOME URL HERE</string>
@ -24,7 +25,8 @@
<string name="volume">Volume: </string>
<string name="up_next">Émission en cours :</string>
<string name="up_next">Up next :</string>
<string name="current_programme">Current programme : </string>
<string name="now_streaming">Now streaming</string>
<string name="error_webView">Error loading WebView. Try downloading Google Chrome on Google Play, or enabling it if you disabled it.</string>
<string name="action_settings">Settings</string>
@ -69,5 +71,6 @@
<string name="sleepClosesApp">Sleep - close app after some time</string>
<string name="setSleepDuration">Set duration (minutes)</string>
<string name="willCloseIn">Will close in %1$d minutes</string>
<string name="fetchPeriod">Set update period when stopped</string>
</resources>

View File

@ -29,4 +29,14 @@
app:defaultValue="false"
/>
<ListPreference
app:key="fetchPeriod"
app:iconSpaceReserved="false"
android:title="@string/fetchPeriod"
app:singleLineTitle="false"
android:entries="@array/fetchPeriodString"
android:entryValues="@array/fetchPeriodValues"
android:defaultValue="10"
/>
</PreferenceScreen>