merged api

remotes/origin/devel
yattoz 2020-01-12 02:16:47 +01:00
commit f133bea0ac
18 changed files with 402 additions and 42 deletions

View File

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

View File

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

View File

@ -22,7 +22,10 @@ class BootBroadcastReceiver : BroadcastReceiver(){
if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" ) if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
{ {
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
if (!PlayerStore.instance.isInitialized)
PlayerStore.instance.initApi()
if (PlayerStore.instance.streamerName.value.isNullOrBlank()) if (PlayerStore.instance.streamerName.value.isNullOrBlank())
PlayerStore.instance.initPicture(context) PlayerStore.instance.initPicture(context)

View File

@ -129,6 +129,12 @@ class MainActivity : BaseActivity() {
colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null)) colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, 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 // Post-UI Launch
if (PlayerStore.instance.isInitialized) if (PlayerStore.instance.isInitialized)
{ {
@ -160,9 +166,6 @@ class MainActivity : BaseActivity() {
isTimerStarted = true isTimerStarted = true
} }
// fetch program
Planning.instance.parseUrl(/* getString(R.string.planning_url) */ context = this)
// initialize the UI // initialize the UI
setTheme(R.style.AppTheme) setTheme(R.style.AppTheme)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)

View File

@ -1,6 +1,7 @@
package fr.forum_thalie.tsumugi package fr.forum_thalie.tsumugi
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import fr.forum_thalie.tsumugi.preferences.* import fr.forum_thalie.tsumugi.preferences.*
@ -38,4 +39,15 @@ class ParametersActivity : BaseActivity() {
.replace(R.id.parameters_host_container, fragmentToLoad) .replace(R.id.parameters_host_container, fragmentToLoad)
.commit() .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

@ -124,10 +124,32 @@ class RadioService : MediaBrowserServiceCompat() {
private val titleObserver = Observer<String> { 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. // 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)
{
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 if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
&& it != noConnectionValue) && it != noConnectionValue)
{ {
PlayerStore.instance.updateLp() PlayerStore.instance.updateLp()
PlayerStore.instance.updateQueue()
} }
nowPlayingNotification.update(this) nowPlayingNotification.update(this)
Planning.instance.checkProgramme() Planning.instance.checkProgramme()
@ -163,6 +185,12 @@ class RadioService : MediaBrowserServiceCompat() {
preferenceStore = PreferenceManager.getDefaultSharedPreferences(this) 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 // Define managers
telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE) telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)

View File

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

View File

@ -7,9 +7,16 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import fr.forum_thalie.tsumugi.* 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 { class PlayerStore {
private lateinit var urlToScrape: String
val isPlaying: MutableLiveData<Boolean> = MutableLiveData() val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData() val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
val volume: MutableLiveData<Int> = MutableLiveData() val volume: MutableLiveData<Int> = MutableLiveData()
@ -27,6 +34,7 @@ class PlayerStore {
val listenersCount: MutableLiveData<Int> = MutableLiveData() val listenersCount: MutableLiveData<Int> = MutableLiveData()
var latencyCompensator : Long = 0 var latencyCompensator : Long = 0
var isInitialized: Boolean = false var isInitialized: Boolean = false
var isStreamDown: Boolean = false
init { init {
playbackState.value = PlaybackStateCompat.STATE_STOPPED playbackState.value = PlaybackStateCompat.STATE_STOPPED
@ -43,10 +51,131 @@ class PlayerStore {
listenersCount.value = 0 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")))
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
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 ############## // ############## QUEUE / LP FUNCTIONS ##############
// ################################################## // ##################################################
fun updateQueue() {
if (queue.isNotEmpty()) {
queue.remove(queue.first())
Log.d(tag, queue.toString())
fetchLastRequest()
isQueueUpdated.value = true
} else if (isInitialized) {
fetchLastRequest()
} else {
Log.d(tag, "queue is empty! fetching anyway !!")
fetchLastRequest()
}
}
fun updateLp() { fun updateLp() {
// note : lp is empty at initialization. This check was needed when we used the R/a/dio API. // note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
//if (lp.isNotEmpty()){ //if (lp.isNotEmpty()){
@ -61,6 +190,74 @@ class PlayerStore {
} }
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())
{
Log.d(tag, playerStoreTag + "Song already in there: $t")
Async(sleepScrape, post)
} else {
queue.add(queue.size, t)
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 ################# // ############## PICTURE FUNCTIONS #################
// ################################################## // ##################################################

View File

@ -1,9 +1,6 @@
package fr.forum_thalie.tsumugi.preferences package fr.forum_thalie.tsumugi.preferences
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.preference.* import androidx.preference.*
import fr.forum_thalie.tsumugi.R import fr.forum_thalie.tsumugi.R
@ -40,5 +37,25 @@ class CustomizeFragment : PreferenceFragmentCompat() {
true 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

@ -49,12 +49,12 @@ class NowPlayingFragment : Fragment() {
val volumeText: TextView = root.findViewById(R.id.volume_text) val volumeText: TextView = root.findViewById(R.id.volume_text)
val progressBar: ProgressBar = root.findViewById(R.id.progressBar) val progressBar: ProgressBar = root.findViewById(R.id.progressBar)
val volumeIconImage : ImageView = root.findViewById(R.id.volume_icon) 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) 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. // 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 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) val streamerNameText : TextView = root.findViewById(R.id.streamerName)
@ -69,15 +69,15 @@ class NowPlayingFragment : Fragment() {
listenersText,8, 16, 2, TypedValue.COMPLEX_UNIT_SP) 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... // 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. // so I observe a dedicated Mutable that gets set when the queue is updated.
PlayerStore.instance.isQueueUpdated.observe(viewLifecycleOwner, Observer { 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 songTitleNextText.text = t.title.value
songArtistNextText.text = t.artist.value songArtistNextText.text = t.artist.value
}) })
/*
PlayerStore.instance.streamerName.observe(viewLifecycleOwner, Observer { PlayerStore.instance.streamerName.observe(viewLifecycleOwner, Observer {
streamerNameText.text = it streamerNameText.text = it
}) })
@ -92,10 +92,12 @@ class NowPlayingFragment : Fragment() {
songTitleText.text = it songTitleText.text = it
}) })
Planning.instance.currentProgramme.observe(viewLifecycleOwner, Observer { Planning.instance.currentProgramme.observe(viewLifecycleOwner, Observer {
songTitleNextText.text = it currentProgrammeText.text = it
}) })
PlayerStore.instance.currentSong.artist.observe(viewLifecycleOwner, Observer { PlayerStore.instance.currentSong.artist.observe(viewLifecycleOwner, Observer {
songArtistText.text = it songArtistText.text = it
}) })

View File

@ -37,7 +37,7 @@ class ProgrammeFragment : Fragment() {
viewPager.adapter = adapter viewPager.adapter = adapter
val todaySundayFirst = Calendar.getInstance(Planning.instance.timeZone).get(Calendar.DAY_OF_WEEK) - 1 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) val tabLayout : TabLayout = root.findViewById(R.id.dayTabLayout)
tabLayout.setupWithViewPager(viewPager) tabLayout.setupWithViewPager(viewPager)

View File

@ -93,13 +93,6 @@
android:visibility="gone" 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 <TextView
android:id="@+id/sleepInfo" android:id="@+id/sleepInfo"
android:layout_width="0dp" android:layout_width="0dp"
@ -162,7 +155,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:fillViewport="true" 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_constraintEnd_toStartOf="@id/streamerPicture"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seek_bar_volume"> app:layout_constraintTop_toBottomOf="@id/seek_bar_volume">
@ -184,7 +178,7 @@
android:layout_gravity="top" android:layout_gravity="top"
android:text="@string/up_next" android:text="@string/up_next"
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/whited3" android:textColor="@color/whited5"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -199,10 +193,10 @@
android:text="" android:text=""
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/whited3" android:textColor="@color/whited3"
android:textSize="16sp" android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/upNext" app:layout_constraintTop_toBottomOf="@id/upNext"
android:visibility="gone"/> android:visibility="visible"/>
<TextView <TextView
android:id="@+id/text_song_title_next" android:id="@+id/text_song_title_next"
@ -212,14 +206,50 @@
android:text="" android:text=""
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/whited" android:textColor="@color/whited"
android:textSize="16sp" android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent" 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> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </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> </androidx.constraintlayout.widget.ConstraintLayout>
@ -268,7 +298,7 @@
android:text="" android:text=""
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/whited" android:textColor="@color/whited"
android:textSize="20sp" android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
@ -280,7 +310,7 @@
android:text="" android:text=""
android:textAlignment="center" android:textAlignment="center"
android:textColor="@color/whited3" android:textColor="@color/whited3"
android:textSize="20sp" android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/text_song_title" app:layout_constraintBottom_toTopOf="@id/text_song_title"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
@ -305,7 +335,7 @@
android:progressDrawable="@drawable/progress_bar_progress" android:progressDrawable="@drawable/progress_bar_progress"
app:layout_constraintBottom_toTopOf="@id/play_pause" app:layout_constraintBottom_toTopOf="@id/play_pause"
tools:layout_editor_absoluteX="0dp" tools:layout_editor_absoluteX="0dp"
android:visibility="gone"/> android:visibility="visible"/>
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR --> <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
<TextView <TextView
@ -318,7 +348,7 @@
android:textAlignment="textEnd" android:textAlignment="textEnd"
app:layout_constraintEnd_toEndOf="@id/progressBar" app:layout_constraintEnd_toEndOf="@id/progressBar"
app:layout_constraintTop_toBottomOf="@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 --> <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
<TextView <TextView
@ -331,7 +361,7 @@
android:textAlignment="textStart" android:textAlignment="textStart"
app:layout_constraintStart_toStartOf="@id/progressBar" app:layout_constraintStart_toStartOf="@id/progressBar"
app:layout_constraintTop_toBottomOf="@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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
app:layout_constraintGuide_percent="0.33" /> app:layout_constraintGuide_percent="0.38" />
<androidx.constraintlayout.widget.Guideline <androidx.constraintlayout.widget.Guideline
android:id="@+id/splitHorizontalLayout" 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="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="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="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> <string name="action_settings">Paramètres</string>
@ -50,5 +51,6 @@
<string name="sleepClosesApp">Minuterie avant fermeture de l\'application</string> <string name="sleepClosesApp">Minuterie avant fermeture de l\'application</string>
<string name="setSleepDuration">Choisir une durée (en minutes)</string> <string name="setSleepDuration">Choisir une durée (en minutes)</string>
<string name="willCloseIn">Extinction dans %1$d 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> </resources>

View File

@ -13,4 +13,19 @@
<item name="30">30</item> <item name="30">30</item>
</string-array> </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> </resources>

View File

@ -11,7 +11,7 @@
<color name="whited3">#999999</color> <color name="whited3">#999999</color>
<color name="whited4">#999999</color> <color name="whited4">#999999</color>
<color name="whited5">#7F7F7F</color> <color name="whited5">#7F7F7F</color>
<color name="rblue">#4FC3F7</color> <color name="rblue">#64B5F6</color>
<color name="bluereq">#527a8e</color> <color name="bluereq">#527a8e</color>
<color name="blueTitle">#527a8e</color> <color name="blueTitle">#527a8e</color>
<color name="reqButtonPressed">#5cb85c</color> <color name="reqButtonPressed">#5cb85c</color>
@ -20,10 +20,10 @@
<color name="seek_bar_background">#585858</color> <color name="seek_bar_background">#585858</color>
<color name="seek_bar_progress">#f58b01</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_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="progress_bar_secondary_progress">#f58b01</color>
<color name="white">#FFFFFF</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="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
<string name="website_url">https://tsumugi.forum-thalie.fr/</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="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> <string name="planning_url">ADD SOME URL HERE</string>
@ -24,7 +25,8 @@
<string name="volume">Volume: </string> <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="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="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> <string name="action_settings">Settings</string>
@ -69,5 +71,6 @@
<string name="sleepClosesApp">Sleep - close app after some time</string> <string name="sleepClosesApp">Sleep - close app after some time</string>
<string name="setSleepDuration">Set duration (minutes)</string> <string name="setSleepDuration">Set duration (minutes)</string>
<string name="willCloseIn">Will close in %1$d minutes</string> <string name="willCloseIn">Will close in %1$d minutes</string>
<string name="fetchPeriod">Set update period when stopped</string>
</resources> </resources>

View File

@ -29,4 +29,14 @@
app:defaultValue="false" 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> </PreferenceScreen>