Merge branch 'tsumugi/master'

remotes/origin/devel
yattoz 2020-02-27 20:46:57 +01:00
commit 8374a7888b
45 ha cambiato i file con 499 aggiunte e 103 eliminazioni

Vedi File

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

Vedi 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"

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 62 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 74 KiB

Vedi File

@ -13,7 +13,7 @@ class Async(val handler: (Any?) -> Any?, val post: (Any?) -> Unit = {},
execute()
} catch (e: Exception)
{
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 = {},
}
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) {
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) {
Log.d(tag,e.toString())
//[REMOVE LOG CALLS]Log.d(tag,e.toString())
onException(e)
}
}

Vedi File

@ -21,7 +21,7 @@ abstract class BaseActivity : AppCompatActivity() {
val height = ((rootLayout?.height ?: 0))
val width = ((rootLayout?.width ?: 0))
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
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
Log.d(tag, "bottomNav visibility set to VISIBLE")
//[REMOVE LOG CALLS]Log.d(tag, "bottomNav visibility set to VISIBLE")
}
protected fun attachKeyboardListeners() {

Vedi File

@ -7,12 +7,13 @@ import android.os.Build
import android.util.Log
import androidx.preference.PreferenceManager
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
import fr.forum_thalie.tsumugi.planning.Planning
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
class BootBroadcastReceiver : BroadcastReceiver(){
override fun onReceive(context: Context, arg1: Intent) {
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 +23,11 @@ class BootBroadcastReceiver : BroadcastReceiver(){
if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
{
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
Planning.instance.parseUrl(context = context)
if (!PlayerStore.instance.isInitialized)
PlayerStore.instance.initApi()
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
PlayerStore.instance.initPicture(context)

Vedi File

@ -15,6 +15,7 @@ import fr.forum_thalie.tsumugi.playerstore.PlayerStore
import java.util.Timer
import android.view.MenuItem
import com.google.android.material.snackbar.Snackbar
import fr.forum_thalie.tsumugi.alarm.RadioAlarm
import fr.forum_thalie.tsumugi.planning.Planning
@ -86,6 +87,14 @@ class MainActivity : BaseActivity() {
true
}
*/
R.id.action_refresh -> {
PlayerStore.instance.queue.clear()
//PlayerStore.instance.lp.clear()
PlayerStore.instance.initApi()
val s = Snackbar.make(findViewById(R.id.nav_host_container), getString(R.string.refreshing) as CharSequence, Snackbar.LENGTH_LONG)
s.show()
true
}
R.id.action_settings -> {
val i = Intent(this, ParametersActivity::class.java)
startActivity(i)
@ -129,10 +138,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)
{
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 +175,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 +205,7 @@ class MainActivity : BaseActivity() {
val i = Intent(this, RadioService::class.java)
i.putExtra("action", a.name)
i.putExtra("value", v)
Log.d(tag, "Sending intent ${a.name}")
//[REMOVE LOG CALLS]Log.d(tag, "Sending intent ${a.name}")
startService(i)
}
@ -238,7 +250,7 @@ class MainActivity : BaseActivity() {
// File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/MyPersonalAppFolder")
val logDirectory = File("$appDirectory/log")
val logFile = File(logDirectory, "logcat" + System.currentTimeMillis() + ".txt")
Log.d(
//[REMOVE LOG CALLS]Log.d(
tag,
"appDirectory : $appDirectory, logDirectory : $logDirectory, logFile : $logFile"
)
@ -246,20 +258,20 @@ class MainActivity : BaseActivity() {
// create app folder
if (!appDirectory.exists()) {
appDirectory.mkdir()
Log.d(tag, "$appDirectory created")
//[REMOVE LOG CALLS]Log.d(tag, "$appDirectory created")
}
// create log folder
if (!logDirectory.exists()) {
logDirectory.mkdir()
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 ")
Log.d(tag, "logcat started")
//[REMOVE LOG CALLS]Log.d(tag, "logcat started")
} catch (e: IOException) {
e.printStackTrace()
}

Vedi 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)
}
}
}

Vedi File

@ -68,14 +68,14 @@ class RadioService : MediaBrowserServiceCompat() {
// This *should* work in any case...
when (intent.getIntExtra("state", -1)) {
0 -> {
Log.d(tag, radioTag + "Headset is unplugged")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is unplugged")
}
1 -> {
Log.d(tag, radioTag + "Headset is plugged")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is plugged")
headsetPluggedIn = true
}
else -> {
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
{
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
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) }
}
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)
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.
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) {
Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
}
if (entry is IcyInfo) {
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++
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())
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()
)
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"
}
Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
}
}

Vedi 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)

Vedi 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)
Log.d(tag, calendar.toString())
//[REMOVE LOG CALLS]Log.d(tag, calendar.toString())
return calendar.timeInMillis

Vedi 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
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)
}
Log.d(tag, "cancelled sleep")
//[REMOVE LOG CALLS]Log.d(tag, "cancelled sleep")
sleepAtMillis.value = null
}
}

Vedi 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)
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 {
Log.d(tag, this.toString())
//[REMOVE LOG CALLS]Log.d(tag, this.toString())
}
}

Vedi File

@ -7,9 +7,17 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.lifecycle.MutableLiveData
import fr.forum_thalie.tsumugi.*
import fr.forum_thalie.tsumugi.planning.Planning
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 +35,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
@ -38,29 +47,215 @@ class PlayerStore {
isQueueUpdated.value = false
isLpUpdated.value = false
isMuted.value = false
currentSong.title.value = noConnectionValue
currentSongBackup.title.value = noConnectionValue
currentSong.setTitleArtist(noConnectionValue)
currentSongBackup.setTitleArtist(noConnectionValue)
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 z", Locale.getDefault())
try {
val t: Date? = dateFormat.parse("$s ${Planning.instance.timeZone.id}")
//[REMOVE LOG CALLS]Log.d(tag, "date: $s -> $t")
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
val apiTime = getTimestamp(res.getJSONObject("station").getString("schedulerTime"))
// 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 = apiTime - (currentSong.startTime.value!!)
//[REMOVE LOG CALLS]Log.d(tag, "latency compensator set to ${(latencyCompensator).toFloat() / 1000} s")
}
currentTime.value = apiTime - (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)
fetchLastRequest()
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() {
//[REMOVE LOG CALLS]Log.d(tag, queue.toString())
fetchLastRequest()
}
fun updateLp() {
// note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
//if (lp.isNotEmpty()){
val n = Song()
n.copy(currentSongBackup)
if (n.title.value != noConnectionValue && n.title.value != streamDownValue)
if (n != Song(noConnectionValue) && n != Song(streamDownValue))
lp.add(0, n)
currentSongBackup.copy(currentSong)
isLpUpdated.value = true
Log.d(tag, playerStoreTag + lp.toString())
//[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + lp.toString())
//}
}
private fun fetchLastRequest()
{
isQueueUpdated.value = false
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")) {
val queueJSON =
resMain.getJSONObject("next")
val t = extractSong(queueJSON)
if (queue.isNotEmpty() && (t == queue.last() || t == currentSong) && isQueueUpdated.value == false)
{
//[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + "Song already in there: $t\nQueue:$queue")
Async(sleepScrape, post)
} else {
if (queue.isNotEmpty())
queue.remove(queue.first())
queue.add(queue.size, t)
//[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + "added last queue song: $t")
isQueueUpdated.value = true
return // FUUUCK IT WAS CALLING THE ASYNC ONE MORE TIME AFTERWARDS !?
}
}
}
}
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 #################
// ##################################################

Vedi File

@ -1,5 +1,6 @@
package fr.forum_thalie.tsumugi.playerstore
import androidx.core.text.HtmlCompat
import androidx.lifecycle.MutableLiveData
import fr.forum_thalie.tsumugi.noConnectionValue
@ -24,8 +25,9 @@ class Song(artistTitle: String = "", _id : Int = 0) {
return "id=$id | ${artist.value} - ${title.value} | type=${type.value} | times ${startTime.value} - ${stopTime.value}\n"
}
fun setTitleArtist(data: String)
fun setTitleArtist(dataHtml: String)
{
val data = HtmlCompat.fromHtml(dataHtml, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
val hyphenPos = data.indexOf(" - ")
try {
if (hyphenPos < 0)
@ -46,12 +48,12 @@ class Song(artistTitle: String = "", _id : Int = 0) {
override fun equals(other: Any?) : Boolean
{
val song: Song = other as Song
return this.title.value == song.title.value && this.artist.value == song.artist.value
return this.title.value === song.title.value && this.artist.value === song.artist.value
}
fun copy(song: Song) {
this.title.value = song.title.value
this.artist.value = song.artist.value
this.title. value = song.title.value
this.startTime.value = song.startTime.value
this.stopTime.value = song.stopTime.value
this.type.value = song.type.value

Vedi 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
}
}
}

Vedi File

@ -40,7 +40,7 @@ class ImageGetterAsyncTask(
override fun doInBackground(vararg params: TextView?): Bitmap? {
t = params[0]
return try {
//Log.d(LOG_CAT, "Downloading the image from: $source")
////[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(LOG_CAT, "Downloading the image from: $source")
var k: InputStream? = null
var pic: Bitmap? = null
try {

Vedi File

@ -43,9 +43,9 @@ class NewsFragment : Fragment() {
}
newsViewModel.isWebViewLoaded = true
Log.d(tag, "webview created")
//[REMOVE LOG CALLS]Log.d(tag, "webview created")
} else {
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)
Log.d(tag, "news fetched onCreate")
//[REMOVE LOG CALLS]Log.d(tag, "news fetched onCreate")
super.onCreate(savedInstanceState)
}
}

Vedi File

@ -43,14 +43,14 @@ class NewsViewModel : ViewModel() {
val maxNumberOfArticles = 5
coroutineScope.launch(Dispatchers.Main) {
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]
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()
Log.d(tag, "$news --- $dateString")
//[REMOVE LOG CALLS]Log.d(tag, "$news --- $dateString")
news.date = formatter6.parse(dateString) ?: Date(0)

Vedi File

@ -6,8 +6,6 @@ import android.content.Context
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.util.TypedValue
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
@ -15,7 +13,6 @@ import android.view.ViewGroup
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.widget.TextViewCompat
import androidx.lifecycle.Observer
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
@ -24,7 +21,6 @@ import fr.forum_thalie.tsumugi.alarm.RadioSleeper
import fr.forum_thalie.tsumugi.planning.Planning
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
import fr.forum_thalie.tsumugi.playerstore.Song
import kotlinx.android.synthetic.main.fragment_nowplaying.*
class NowPlayingFragment : Fragment() {
@ -49,12 +45,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.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 +65,14 @@ 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 +87,12 @@ class NowPlayingFragment : Fragment() {
songTitleText.text = it
})
Planning.instance.currentProgramme.observe(viewLifecycleOwner, Observer {
songTitleNextText.text = it
currentProgrammeText.text = "${context!!.getString(R.string.current_programme)} $it"
})
PlayerStore.instance.currentSong.artist.observe(viewLifecycleOwner, Observer {
songArtistText.text = it
})
@ -273,7 +270,7 @@ class NowPlayingFragment : Fragment() {
(viewHeight*100)/viewWidth
else
100
Log.d(tag, "orientation set")
//[REMOVE LOG CALLS]Log.d(tag, "orientation set")
}
override fun onResume() {

Vedi 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)
Log.d(tag, "SongFragment view created")
//[REMOVE LOG CALLS]Log.d(tag, "SongFragment view created")
return root
}

Vedi File

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

Vedi File

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

Vedi File

@ -36,7 +36,7 @@
android:contentDescription="dj-image"
android:scaleType="fitStart"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="@id/scrollViewMetadataNext"
app:layout_constraintStart_toStartOf="@id/imageGuideline"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/logo_roundsquare"
@ -49,7 +49,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.68" />
app:layout_constraintGuide_percent="0.71" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/imageLeftGuideline"
@ -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"
@ -147,7 +140,7 @@
<ImageView
android:id="@+id/volume_icon"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_height="20dp"
android:contentDescription="@string/volume"
android:src="@drawable/ic_volume_high"
android:textSize="12sp"
@ -162,10 +155,12 @@
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/scrollProgramme"
app:layout_constraintEnd_toStartOf="@id/streamerPicture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seek_bar_volume">
app:layout_constraintTop_toBottomOf="@id/seek_bar_volume"
>
<androidx.constraintlayout.widget.ConstraintLayout
@ -184,8 +179,8 @@
android:layout_gravity="top"
android:text="@string/up_next"
android:textAlignment="center"
android:textColor="@color/whited3"
android:layout_marginTop="8dp"
android:textColor="@color/whited5"
android:layout_marginTop="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -202,7 +197,7 @@
android:textSize="16sp"
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"
@ -214,13 +209,75 @@
android:textColor="@color/whited"
android:textSize="16sp"
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>
<ScrollView
android:id="@+id/scrollProgramme"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fillViewport="true"
android:layout_marginBottom="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/scrollViewMetadataNext"
app:layout_constraintBottom_toBottomOf="@id/topInfoGuideline"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:visibility="visible"
>
<TextView
android:id="@+id/current_programme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top|center_horizontal"
android:text="@string/current_programme"
android:textAlignment="center"
android:textColor="@color/whited3"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
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|center_horizontal"
android:textAlignment="textStart"
android:textColor="@color/whited"
android:textSize="16sp"
app:layout_constraintStart_toEndOf="@id/current_programme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<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
@ -305,7 +362,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 +375,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 +388,7 @@
android:textAlignment="textStart"
app:layout_constraintStart_toStartOf="@id/progressBar"
app:layout_constraintTop_toBottomOf="@id/progressBar"
android:visibility="gone"/>
android:visibility="visible"/>
@ -346,7 +403,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.58" />
app:layout_constraintGuide_percent="0.63" />
<ImageButton
@ -381,7 +438,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"

Vedi File

@ -15,23 +15,22 @@
app:showAsAction="ifRoom"/>
<!--
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never"/>
-->
<!--
<item
android:id="@+id/action_bug_submit"
android:title="@string/action_bug_submit"
app:showAsAction="never"/>
-->
<item
android:id="@+id/action_refresh"
android:title="@string/action_refresh"
app:showAsAction="never"/>
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:icon="@drawable/ic_settings"
app:showAsAction="ifRoom"/>
app:showAsAction="never"/>
</menu>

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 3.8 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 5.0 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 6.1 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 7.5 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 6.2 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 6.9 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 2.3 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 2.9 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 3.3 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 4.2 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 3.5 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 3.9 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 5.9 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 7.5 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 9.2 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 11 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 9.5 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 10 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 10 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 13 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 16 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 19 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 16 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 18 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 16 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 19 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 23 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 29 KiB

File binario non mostrato.

Prima

Larghezza:  |  Altezza:  |  Dimensione: 25 KiB

Dopo

Larghezza:  |  Altezza:  |  Dimensione: 26 KiB

Vedi 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>

Vedi File

@ -10,7 +10,8 @@
<string name="volume">Volume : </string>
<string name="up_next">Émission en cours :</string>
<string name="up_next">À suivre :</string>
<string name="current_programme">Émission : </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,8 @@
<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>
<string name="refreshing">Actualisation…</string>
<string name="action_refresh">Raffraîchir les données</string>
</resources>

Vedi 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>

Vedi 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,8 @@
<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>
<string name="refreshing">Refreshing data…</string>
<string name="action_refresh">Refresh data</string>
</resources>

Vedi 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>