Browse Source

Merge branch 'tsumugi/master' into tsumugi/release

yattoz 4 years ago
parent
commit
ae1d1a1e44
27 changed files with 446 additions and 86 deletions
  1. 2 2
      app/build.gradle
  2. 0 3
      app/src/main/AndroidManifest.xml
  3. 4 4
      app/src/main/java/fr/forum_thalie/tsumugi/Async.kt
  4. 3 3
      app/src/main/java/fr/forum_thalie/tsumugi/BaseActivity.kt
  5. 4 1
      app/src/main/java/fr/forum_thalie/tsumugi/BootBroadcastReceiver.kt
  6. 12 9
      app/src/main/java/fr/forum_thalie/tsumugi/MainActivity.kt
  7. 12 0
      app/src/main/java/fr/forum_thalie/tsumugi/ParametersActivity.kt
  8. 42 14
      app/src/main/java/fr/forum_thalie/tsumugi/RadioService.kt
  9. 10 0
      app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt
  10. 1 1
      app/src/main/java/fr/forum_thalie/tsumugi/alarm/RadioAlarm.kt
  11. 2 2
      app/src/main/java/fr/forum_thalie/tsumugi/alarm/RadioSleeper.kt
  12. 2 2
      app/src/main/java/fr/forum_thalie/tsumugi/planning/Programme.kt
  13. 198 1
      app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt
  14. 20 3
      app/src/main/java/fr/forum_thalie/tsumugi/preferences/CustomizeFragment.kt
  15. 3 3
      app/src/main/java/fr/forum_thalie/tsumugi/ui/news/NewsFragment.kt
  16. 3 3
      app/src/main/java/fr/forum_thalie/tsumugi/ui/news/NewsViewModel.kt
  17. 9 7
      app/src/main/java/fr/forum_thalie/tsumugi/ui/nowplaying/NowPlayingFragment.kt
  18. 2 2
      app/src/main/java/fr/forum_thalie/tsumugi/ui/programme/ProgrammeFragment.kt
  19. 1 1
      app/src/main/java/fr/forum_thalie/tsumugi/ui/songs/SongsFragment.kt
  20. 1 1
      app/src/main/java/fr/forum_thalie/tsumugi/ui/songs/queuelp/LastPlayedFragment.kt
  21. 49 19
      app/src/main/res/layout/fragment_nowplaying.xml
  22. 31 0
      app/src/main/res/values-fr/arrays.xml
  23. 3 1
      app/src/main/res/values-fr/strings.xml
  24. 15 0
      app/src/main/res/values/arrays.xml
  25. 3 3
      app/src/main/res/values/colors.xml
  26. 4 1
      app/src/main/res/values/strings.xml
  27. 10 0
      app/src/main/res/xml/customize_preferences.xml

+ 2 - 2
app/build.gradle View File

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

+ 0 - 3
app/src/main/AndroidManifest.xml View File

@@ -31,9 +31,6 @@
31 31
                 <action android:name="android.media.browse.MediaBrowserService" />
32 32
             </intent-filter>
33 33
         </service>
34
-        <service android:name=".streamerNotificationService.StreamerMonitorService"
35
-            android:enabled="true"
36
-            android:exported="true"/>
37 34
 
38 35
         <receiver android:name=".BootBroadcastReceiver"
39 36
             android:directBootAware="true"

+ 4 - 4
app/src/main/java/fr/forum_thalie/tsumugi/Async.kt View File

@@ -13,7 +13,7 @@ class Async(val handler: (Any?) -> Any?, val post: (Any?) -> Unit = {},
13 13
             execute()
14 14
         } catch (e: Exception)
15 15
         {
16
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag,e.toString())
16
+            //[REMOVE LOG CALLS]Log.d(tag,e.toString())
17 17
         }
18 18
     }
19 19
 
@@ -46,14 +46,14 @@ class Async(val handler: (Any?) -> Any?, val post: (Any?) -> Unit = {},
46 46
         }
47 47
 
48 48
 
49
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "fallback for no network. Store reset : $storeReset")
49
+        //[REMOVE LOG CALLS]Log.d(tag, "fallback for no network. Store reset : $storeReset")
50 50
     }
51 51
 
52 52
     override fun doInBackground(vararg params: Any?): Any? {
53 53
         try {
54 54
             return handler(parameters)
55 55
         } catch (e: Exception) {
56
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag,e.toString())
56
+            //[REMOVE LOG CALLS]Log.d(tag,e.toString())
57 57
             onException(e)
58 58
         }
59 59
         return null
@@ -63,7 +63,7 @@ class Async(val handler: (Any?) -> Any?, val post: (Any?) -> Unit = {},
63 63
         try {
64 64
             post(result)
65 65
         } catch (e: Exception) {
66
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag,e.toString())
66
+            //[REMOVE LOG CALLS]Log.d(tag,e.toString())
67 67
             onException(e)
68 68
         }
69 69
     }

+ 3 - 3
app/src/main/java/fr/forum_thalie/tsumugi/BaseActivity.kt View File

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

+ 4 - 1
app/src/main/java/fr/forum_thalie/tsumugi/BootBroadcastReceiver.kt View File

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

+ 12 - 9
app/src/main/java/fr/forum_thalie/tsumugi/MainActivity.kt View File

@@ -129,10 +129,16 @@ class MainActivity : BaseActivity() {
129 129
         colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
130 130
         colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
131 131
 
132
+        // fetch program
133
+        Planning.instance.parseUrl(/* getString(R.string.planning_url) */ context = this)
134
+
135
+        PlayerStore.instance.initUrl(this)
136
+        PlayerStore.instance.initApi()
137
+
132 138
         // Post-UI Launch
133 139
         if (PlayerStore.instance.isInitialized)
134 140
         {
135
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "skipped initialization")
141
+            //[REMOVE LOG CALLS]Log.d(tag, "skipped initialization")
136 142
         } else {
137 143
             // if the service is not started, start it in STOP mode.
138 144
             // 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() {
160 166
             isTimerStarted = true
161 167
         }
162 168
 
163
-        // fetch program
164
-        Planning.instance.parseUrl(/* getString(R.string.planning_url) */ context = this)
165
-
166 169
         // initialize the UI
167 170
         setTheme(R.style.AppTheme)
168 171
         setContentView(R.layout.activity_main)
@@ -193,7 +196,7 @@ class MainActivity : BaseActivity() {
193 196
             val i = Intent(this, RadioService::class.java)
194 197
             i.putExtra("action", a.name)
195 198
             i.putExtra("value", v)
196
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "Sending intent ${a.name}")
199
+            //[REMOVE LOG CALLS]Log.d(tag, "Sending intent ${a.name}")
197 200
             startService(i)
198 201
     }
199 202
 
@@ -238,7 +241,7 @@ class MainActivity : BaseActivity() {
238 241
                 // File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/MyPersonalAppFolder")
239 242
                 val logDirectory = File("$appDirectory/log")
240 243
                 val logFile = File(logDirectory, "logcat" + System.currentTimeMillis() + ".txt")
241
-                //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(
244
+                //[REMOVE LOG CALLS]Log.d(
242 245
                     tag,
243 246
                     "appDirectory : $appDirectory, logDirectory : $logDirectory, logFile : $logFile"
244 247
                 )
@@ -246,20 +249,20 @@ class MainActivity : BaseActivity() {
246 249
                 // create app folder
247 250
                 if (!appDirectory.exists()) {
248 251
                     appDirectory.mkdir()
249
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$appDirectory created")
252
+                    //[REMOVE LOG CALLS]Log.d(tag, "$appDirectory created")
250 253
                 }
251 254
 
252 255
                 // create log folder
253 256
                 if (!logDirectory.exists()) {
254 257
                     logDirectory.mkdir()
255
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$logDirectory created")
258
+                    //[REMOVE LOG CALLS]Log.d(tag, "$logDirectory created")
256 259
                 }
257 260
 
258 261
                 // clear the previous logcat and then write the new one to the file
259 262
                 try {
260 263
                     Runtime.getRuntime().exec("logcat -c")
261 264
                     Runtime.getRuntime().exec("logcat -v time -f $logFile *:E $tag:V ")
262
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "logcat started")
265
+                    //[REMOVE LOG CALLS]Log.d(tag, "logcat started")
263 266
                 } catch (e: IOException) {
264 267
                     e.printStackTrace()
265 268
                 }

+ 12 - 0
app/src/main/java/fr/forum_thalie/tsumugi/ParametersActivity.kt View File

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

+ 42 - 14
app/src/main/java/fr/forum_thalie/tsumugi/RadioService.kt View File

@@ -68,14 +68,14 @@ class RadioService : MediaBrowserServiceCompat() {
68 68
                 // This *should* work in any case...
69 69
                 when (intent.getIntExtra("state", -1)) {
70 70
                 0 -> {
71
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is unplugged")
71
+                    //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is unplugged")
72 72
                 }
73 73
                 1 -> {
74
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is plugged")
74
+                    //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Headset is plugged")
75 75
                     headsetPluggedIn = true
76 76
                 }
77 77
                 else -> {
78
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "I have no idea what the headset state is")
78
+                    //[REMOVE LOG CALLS]Log.d(tag, radioTag + "I have no idea what the headset state is")
79 79
                 }
80 80
                 }
81 81
                 /*
@@ -91,7 +91,7 @@ class RadioService : MediaBrowserServiceCompat() {
91 91
                 }
92 92
                 else
93 93
                 {
94
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Can't get state?")
94
+                    //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Can't get state?")
95 95
                 }
96 96
 
97 97
                  */
@@ -124,10 +124,32 @@ class RadioService : MediaBrowserServiceCompat() {
124 124
 
125 125
     private val titleObserver = Observer<String> {
126 126
         // We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
127
+
128
+        if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
129
+        {
130
+            //[REMOVE LOG CALLS]Log.d((tag, radioTag + "SONG CHANGED AND PLAYING")
131
+            // we activate latency compensation only if it's been at least 2 songs...
132
+            when {
133
+                PlayerStore.instance.isStreamDown -> {
134
+                    // if we reach here, it means that the observer has been called by a new song and that the stream was down previously.
135
+                    // so the stream is now back to normal.
136
+                    PlayerStore.instance.isStreamDown = false
137
+                    PlayerStore.instance.initApi()
138
+                }
139
+                PlayerStore.instance.currentSong.title.value == noConnectionValue -> {
140
+                    PlayerStore.instance.isStreamDown = true
141
+                }
142
+                else -> {
143
+                    PlayerStore.instance.fetchApi(numberOfSongs >= 2)
144
+                }
145
+            }
146
+        }
147
+
127 148
         if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
128 149
             && it != noConnectionValue)
129 150
         {
130 151
             PlayerStore.instance.updateLp()
152
+            PlayerStore.instance.updateQueue()
131 153
         }
132 154
         nowPlayingNotification.update(this)
133 155
         Planning.instance.checkProgramme()
@@ -163,6 +185,12 @@ class RadioService : MediaBrowserServiceCompat() {
163 185
 
164 186
         preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
165 187
 
188
+        // start ticker for when the player is stopped
189
+        val periodString = PreferenceManager.getDefaultSharedPreferences(this).getString("fetchPeriod", "10") ?: "10"
190
+        val period: Long = Integer.parseInt(periodString).toLong()
191
+        if (period > 0)
192
+            apiTicker.schedule(ApiFetchTick(), 0, period * 1000)
193
+
166 194
         // Define managers
167 195
         telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
168 196
         telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
@@ -205,7 +233,7 @@ class RadioService : MediaBrowserServiceCompat() {
205 233
         startForeground(radioServiceId, nowPlayingNotification.notification)
206 234
 
207 235
         PlayerStore.instance.isServiceStarted.value = true
208
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "created")
236
+        //[REMOVE LOG CALLS]Log.d(tag, radioTag + "created")
209 237
     }
210 238
 
211 239
     private val handler = Handler()
@@ -246,7 +274,7 @@ class RadioService : MediaBrowserServiceCompat() {
246 274
             Actions.CANCEL_FADE_OUT.name -> { handler.removeCallbacks(lowerVolumeRunnable) }
247 275
             Actions.SNOOZE.name -> { RadioAlarm.instance.snooze(this) }
248 276
         }
249
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "intent received : " + intent.getStringExtra("action"))
277
+        //[REMOVE LOG CALLS]Log.d(tag, radioTag + "intent received : " + intent.getStringExtra("action"))
250 278
         super.onStartCommand(intent, flags, startId)
251 279
         // 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.
252 280
         return START_STICKY
@@ -258,7 +286,7 @@ class RadioService : MediaBrowserServiceCompat() {
258 286
             stopSelf()
259 287
         }
260 288
         super.onTaskRemoved(rootIntent)
261
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "task removed")
289
+        //[REMOVE LOG CALLS]Log.d(tag, radioTag + "task removed")
262 290
     }
263 291
 
264 292
     override fun onDestroy() {
@@ -290,7 +318,7 @@ class RadioService : MediaBrowserServiceCompat() {
290 318
         }
291 319
 
292 320
         apiTicker.cancel() // stops the timer.
293
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "destroyed")
321
+        //[REMOVE LOG CALLS]Log.d(tag, radioTag + "destroyed")
294 322
         // if the service is destroyed, the application had become useless.
295 323
         exitProcess(0)
296 324
     }
@@ -361,10 +389,10 @@ class RadioService : MediaBrowserServiceCompat() {
361 389
             for (i in 0 until it.length()) {
362 390
                 val entry  = it.get(i)
363 391
                 if (entry is IcyHeaders) {
364
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
392
+                    //[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: IcyHeaders $entry")
365 393
                 }
366 394
                 if (entry is IcyInfo) {
367
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: Title ----> ${entry.title}")
395
+                    //[REMOVE LOG CALLS]Log.d(tag, radioTag + "onMetadata: Title ----> ${entry.title}")
368 396
                     // Note : Kotlin supports UTF-8 by default.
369 397
                     numberOfSongs++
370 398
                     val data = entry.title!!
@@ -439,7 +467,7 @@ class RadioService : MediaBrowserServiceCompat() {
439 467
             {
440 468
                 Thread.sleep(1000)
441 469
                 i++
442
-                //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$i, isAlarmStopped=$isAlarmStopped")
470
+                //[REMOVE LOG CALLS]Log.d(tag, "$i, isAlarmStopped=$isAlarmStopped")
443 471
             }
444 472
         }
445 473
         val post: (Any?) -> Unit = {
@@ -512,7 +540,7 @@ class RadioService : MediaBrowserServiceCompat() {
512 540
             SystemClock.elapsedRealtime()
513 541
         )
514 542
         mediaSession.setPlaybackState(playbackStateBuilder.build())
515
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "begin playing")
543
+        //[REMOVE LOG CALLS]Log.d(tag, radioTag + "begin playing")
516 544
     }
517 545
 
518 546
     private fun pausePlaying()
@@ -541,7 +569,7 @@ class RadioService : MediaBrowserServiceCompat() {
541 569
             1.0f,
542 570
             SystemClock.elapsedRealtime()
543 571
         )
544
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "stopped")
572
+        //[REMOVE LOG CALLS]Log.d(tag, radioTag + "stopped")
545 573
 
546 574
         mediaSession.setPlaybackState(playbackStateBuilder.build())
547 575
     }
@@ -629,7 +657,7 @@ class RadioService : MediaBrowserServiceCompat() {
629 657
                 Player.STATE_ENDED -> state = "Player.STATE_ENDED"
630 658
                 Player.STATE_READY -> state = "Player.STATE_READY"
631 659
             }
632
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
660
+            //[REMOVE LOG CALLS]Log.d(tag, radioTag + "Player changed state: ${state}. numberOfSongs reset.")
633 661
         }
634 662
     }
635 663
 

+ 10 - 0
app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt View File

@@ -1,8 +1,18 @@
1 1
 package fr.forum_thalie.tsumugi
2 2
 
3
+import android.support.v4.media.session.PlaybackStateCompat
3 4
 import fr.forum_thalie.tsumugi.playerstore.PlayerStore
4 5
 import java.util.*
5 6
 
7
+class ApiFetchTick  : TimerTask() {
8
+    override fun run() {
9
+        if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
10
+        {
11
+            PlayerStore.instance.fetchApi()
12
+        }
13
+    }
14
+}
15
+
6 16
 class Tick  : TimerTask() {
7 17
     override fun run() {
8 18
         PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)

+ 1 - 1
app/src/main/java/fr/forum_thalie/tsumugi/alarm/RadioAlarm.kt View File

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

+ 2 - 2
app/src/main/java/fr/forum_thalie/tsumugi/alarm/RadioSleeper.kt View File

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

+ 2 - 2
app/src/main/java/fr/forum_thalie/tsumugi/planning/Programme.kt View File

@@ -35,7 +35,7 @@ class Programme (val title: String, private val periodicity: Int, private val ho
35 35
         val isSpanningOverNight =
36 36
             (((0b1000000 shr ((currentDay - 1) % 7) and (periodicity)) != 0) && hourEnd < hourBegin)
37 37
 
38
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$title is today: $isToday or spanning $isSpanningOverNight")
38
+        //[REMOVE LOG CALLS]Log.d(tag, "$title is today: $isToday or spanning $isSpanningOverNight")
39 39
         // shr = shift-right. It's a binary mask.
40 40
 
41 41
         // 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
87 87
      */
88 88
 
89 89
     init {
90
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, this.toString())
90
+        //[REMOVE LOG CALLS]Log.d(tag, this.toString())
91 91
     }
92 92
 }

+ 198 - 1
app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt View File

@@ -7,9 +7,16 @@ import android.support.v4.media.session.PlaybackStateCompat
7 7
 import android.util.Log
8 8
 import androidx.lifecycle.MutableLiveData
9 9
 import fr.forum_thalie.tsumugi.*
10
+import org.json.JSONObject
11
+import java.net.URL
12
+import java.text.ParseException
13
+import java.text.SimpleDateFormat
14
+import java.util.*
15
+import kotlin.collections.ArrayList
10 16
 
11 17
 class PlayerStore {
12 18
 
19
+    private lateinit var urlToScrape: String
13 20
     val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
14 21
     val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
15 22
     val volume: MutableLiveData<Int> = MutableLiveData()
@@ -27,6 +34,7 @@ class PlayerStore {
27 34
     val listenersCount: MutableLiveData<Int> = MutableLiveData()
28 35
     var latencyCompensator : Long = 0
29 36
     var isInitialized: Boolean = false
37
+    var isStreamDown: Boolean = false
30 38
 
31 39
     init {
32 40
         playbackState.value = PlaybackStateCompat.STATE_STOPPED
@@ -43,10 +51,131 @@ class PlayerStore {
43 51
         listenersCount.value = 0
44 52
     }
45 53
 
54
+    fun initUrl(c: Context)
55
+    {
56
+        urlToScrape = c.getString(R.string.API_URL)
57
+    }
58
+
59
+    private fun getTimestamp(s: String) : Long
60
+    {
61
+        val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
62
+        try {
63
+            val t: Date? = dateFormat.parse(s)
64
+            return t!!.time
65
+        } catch (e: ParseException) {
66
+            e.printStackTrace()
67
+        }
68
+        return 0
69
+    }
70
+
71
+
72
+    // ##################################################
73
+    // ################# API FUNCTIONS ##################
74
+    // ##################################################
75
+
76
+    private fun updateApi(res: JSONObject, isCompensatingLatency : Boolean = false) {
77
+        // If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that.
78
+
79
+        val resMain = res.getJSONObject("tracks").getJSONObject("current")
80
+        val s = extractSong(resMain)
81
+        if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
82
+            || currentSong.title.value == noConnectionValue)
83
+            currentSong.setTitleArtist("${s.artist.value} - ${s.title.value}")
84
+
85
+        val starts = s.startTime.value
86
+        val ends = s.stopTime.value
87
+
88
+        if (currentSong.startTime.value != starts)
89
+            currentSong.startTime.value = starts
90
+
91
+        currentSong.stopTime.value = ends
92
+
93
+        // I noticed that the server has a big (3 to 9 seconds !!) offset for current time.
94
+        // we can measure it when the player is playing, to compensate it and have our progress bar perfectly timed
95
+        // 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),
96
+        // 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.
97
+        if(isCompensatingLatency)
98
+        {
99
+            latencyCompensator = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (currentSong.startTime.value ?: getTimestamp(res.getJSONObject("station").getString("schedulerTime")))
100
+            //[REMOVE LOG CALLS]Log.d((tag, "latency compensator set to ${(latencyCompensator).toFloat()/1000} s")
101
+        }
102
+        currentTime.value = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (latencyCompensator)
103
+
104
+        /*
105
+        val listeners = resMain.getInt("listeners")
106
+        listenersCount.value = listeners
107
+        //[REMOVE LOG CALLS]Log.d((tag, playerStoreTag +  "store updated")
108
+         */
109
+    }
110
+
111
+    private val scrape : (Any?) -> String =
112
+        {
113
+            URL(urlToScrape).readText()
114
+        }
115
+
116
+    /* initApi is called :
117
+        - at startup
118
+        - when a streamer changes.
119
+        the idea is to fetch the queue when a streamer changes (potentially Hanyuu), and at startup.
120
+        The Last Played is only fetched if it's empty (so, only at startup), not when a streamer changes.
121
+     */
122
+    fun initApi()
123
+    {
124
+        val post : (parameter: Any?) -> Unit = {
125
+            val result = JSONObject(it as String)
126
+            if (result.has("tracks"))
127
+            {
128
+                updateApi(result)
129
+                currentSongBackup.copy(currentSong)
130
+
131
+                isQueueUpdated.value = true
132
+
133
+                isLpUpdated.value = true
134
+            }
135
+            isInitialized = true
136
+        }
137
+        Async(scrape, post)
138
+    }
139
+
140
+    fun fetchApi(isCompensatingLatency: Boolean = false) {
141
+        val post: (parameter: Any?) -> Unit = {
142
+            val result = JSONObject(it as String)
143
+            if (!result.isNull("tracks"))
144
+            {
145
+                updateApi(result, isCompensatingLatency)
146
+            }
147
+        }
148
+        Async(scrape, post)
149
+    }
150
+
151
+    private fun extractSong(songJSON: JSONObject) : Song {
152
+        val song = Song()
153
+        song.setTitleArtist(songJSON.getString("name"))
154
+        song.startTime.value = getTimestamp(songJSON.getString("starts"))
155
+        song.stopTime.value = getTimestamp(songJSON.getString("ends"))
156
+        song.type.value = 0 // only used for R/a/dio
157
+        return song
158
+    }
159
+
160
+
46 161
     // ##################################################
47 162
     // ############## QUEUE / LP FUNCTIONS ##############
48 163
     // ##################################################
49 164
 
165
+    fun updateQueue() {
166
+        if (queue.isNotEmpty()) {
167
+            queue.remove(queue.first())
168
+            //[REMOVE LOG CALLS]Log.d((tag, queue.toString())
169
+            fetchLastRequest()
170
+            isQueueUpdated.value = true
171
+        } else if (isInitialized) {
172
+            fetchLastRequest()
173
+        } else {
174
+            //[REMOVE LOG CALLS]Log.d((tag,  "queue is empty! fetching anyway !!")
175
+            fetchLastRequest()
176
+        }
177
+    }
178
+
50 179
     fun updateLp() {
51 180
         // note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
52 181
         //if (lp.isNotEmpty()){
@@ -56,11 +185,79 @@ class PlayerStore {
56 185
                 lp.add(0, n)
57 186
             currentSongBackup.copy(currentSong)
58 187
             isLpUpdated.value = true
59
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, playerStoreTag +  lp.toString())
188
+            //[REMOVE LOG CALLS]Log.d(tag, playerStoreTag +  lp.toString())
60 189
         //}
61 190
     }
62 191
 
63 192
 
193
+    private fun fetchLastRequest()
194
+    {
195
+        val sleepScrape: (Any?) -> String = {
196
+            /* 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.
197
+             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)
198
+             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.
199
+            This way to fetch at the most probable time is a good compromise between fetch speed and fetch frequency
200
+            We don't fetch too often, and we start to fetch at the most *probable* time.
201
+            If there's no latencyCompensator measured yet, we only wait for 3 seconds.
202
+            If the song is the same, it will be called again. 3 seconds is a good compromise between speed and frequency:
203
+            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.
204
+             */
205
+            val sleepTime: Long = if (latencyCompensator > 0) latencyCompensator + 2000 else 3000
206
+            Thread.sleep(sleepTime) // we wait a bit (10s) for the API to get updated on R/a/dio side!
207
+            URL(urlToScrape).readText()
208
+        }
209
+
210
+        lateinit var post: (parameter: Any?) -> Unit
211
+
212
+        fun postFun(result: JSONObject)
213
+        {
214
+            if (result.has("tracks")) {
215
+                val resMain = result.getJSONObject("tracks")
216
+                /*
217
+                if ((resMain.has("isafkstream") && !resMain.getBoolean("isafkstream")) &&
218
+                    queue.isNotEmpty())
219
+                {
220
+                    queue.clear() //we're not requesting anything anymore.
221
+                    isQueueUpdated.value = true
222
+                } else if (resMain.has("isafkstream") && resMain.getBoolean("isafkstream") &&
223
+                    queue.isEmpty())
224
+                {
225
+                    initApi()
226
+                } else
227
+                */
228
+                if (resMain.has("next") /*&& queue.isNotEmpty()*/) {
229
+                    val queueJSON =
230
+                        resMain.getJSONObject("next")
231
+                    val t = extractSong(queueJSON)
232
+                    if (queue.isNotEmpty() && t == queue.last())
233
+                    {
234
+                        //[REMOVE LOG CALLS]Log.d((tag, playerStoreTag +  "Song already in there: $t")
235
+                        Async(sleepScrape, post)
236
+                    } else {
237
+                        queue.add(queue.size, t)
238
+                        //[REMOVE LOG CALLS]Log.d(tag, playerStoreTag +  "added last queue song: $t")
239
+                        isQueueUpdated.value = true
240
+                    }
241
+                }
242
+            }
243
+        }
244
+
245
+        post = {
246
+            val result = JSONObject(it as String)
247
+            /*  The goal is to pass the result to a function that will process it (postFun).
248
+                The magic trick is, under circumstances, the last queue song might not have been updated yet when we fetch it.
249
+                So if this is detected ==> if (t == queue.last() )
250
+                Then the function re-schedule an Async(sleepScrape, post).
251
+                To do that, the "post" must be defined BEFORE the function, but the function must be defined BEFORE the "post" value.
252
+                So I declare "post" as lateinit var, define the function, then define the "post" that calls the function. IT SHOULD WORK.
253
+             */
254
+            postFun(result)
255
+        }
256
+
257
+        Async(sleepScrape, post)
258
+    }
259
+
260
+
64 261
     // ##################################################
65 262
     // ############## PICTURE FUNCTIONS #################
66 263
     // ##################################################

+ 20 - 3
app/src/main/java/fr/forum_thalie/tsumugi/preferences/CustomizeFragment.kt View File

@@ -1,9 +1,6 @@
1 1
 package fr.forum_thalie.tsumugi.preferences
2 2
 
3
-import android.content.Intent
4
-import android.net.Uri
5 3
 import android.os.Bundle
6
-import android.util.Log
7 4
 import androidx.appcompat.app.AlertDialog
8 5
 import androidx.preference.*
9 6
 import fr.forum_thalie.tsumugi.R
@@ -40,5 +37,25 @@ class CustomizeFragment : PreferenceFragmentCompat() {
40 37
             true
41 38
         }
42 39
 
40
+
41
+        val fetchPeriod = preferenceScreen.findPreference<ListPreference>("fetchPeriod")
42
+        fetchPeriod?.summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
43
+        fetchPeriod?.setOnPreferenceChangeListener { _, newValue ->
44
+            val builder1 = AlertDialog.Builder(context!!)
45
+            if (Integer.parseInt(newValue as String) == 0)
46
+                builder1.setMessage(R.string.restart_the_app)
47
+            else
48
+                builder1.setMessage(R.string.restart_the_app)
49
+            builder1.setCancelable(true)
50
+
51
+            builder1.setPositiveButton("Close" ) { dialog, _ ->
52
+                dialog.cancel()
53
+            }
54
+
55
+            val alert11 = builder1.create()
56
+            alert11.show()
57
+            true
58
+        }
59
+
43 60
     }
44 61
 }

+ 3 - 3
app/src/main/java/fr/forum_thalie/tsumugi/ui/news/NewsFragment.kt View File

@@ -43,9 +43,9 @@ class NewsFragment : Fragment() {
43 43
                 }
44 44
 
45 45
                 newsViewModel.isWebViewLoaded = true
46
-                //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "webview created")
46
+                //[REMOVE LOG CALLS]Log.d(tag, "webview created")
47 47
             } else {
48
-                //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "webview already created!?")
48
+                //[REMOVE LOG CALLS]Log.d(tag, "webview already created!?")
49 49
             }
50 50
 
51 51
             newsViewModel.root.addOnLayoutChangeListener(orientationLayoutListener)
@@ -101,7 +101,7 @@ class NewsFragment : Fragment() {
101 101
             ViewModelProviders.of(this).get(NewsViewModel::class.java)
102 102
 
103 103
         newsViewModel.fetch(c = context!!, isPreloading = true)
104
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "news fetched onCreate")
104
+        //[REMOVE LOG CALLS]Log.d(tag, "news fetched onCreate")
105 105
         super.onCreate(savedInstanceState)
106 106
     }
107 107
 }

+ 3 - 3
app/src/main/java/fr/forum_thalie/tsumugi/ui/news/NewsViewModel.kt View File

@@ -43,14 +43,14 @@ class NewsViewModel : ViewModel() {
43 43
 
44 44
         val maxNumberOfArticles = 5
45 45
         coroutineScope.launch(Dispatchers.Main) {
46
-            //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "launching coroutine")
46
+            //[REMOVE LOG CALLS]Log.d(tag, "launching coroutine")
47 47
                 val parser = Parser()
48 48
             try {
49 49
                 val articleList = parser.getArticles(urlToScrape)
50 50
                 newsArray.clear()
51 51
                 for (i in 0 until min(articleList.size, maxNumberOfArticles)) {
52 52
                     val item = articleList[i]
53
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "i = $i / ${articleList.size}")
53
+                    //[REMOVE LOG CALLS]Log.d(tag, "i = $i / ${articleList.size}")
54 54
                     val news = News()
55 55
                     news.title = item.title ?: ""
56 56
                     news.link = item.link ?: urlToScrape
@@ -60,7 +60,7 @@ class NewsViewModel : ViewModel() {
60 60
 
61 61
                     val formatter6 = SimpleDateFormat(newsDateTimePattern, Locale.ENGLISH)
62 62
                     val dateString = item.pubDate.toString()
63
-                    //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "$news --- $dateString")
63
+                    //[REMOVE LOG CALLS]Log.d(tag, "$news --- $dateString")
64 64
 
65 65
                     news.date = formatter6.parse(dateString) ?: Date(0)
66 66
 

+ 9 - 7
app/src/main/java/fr/forum_thalie/tsumugi/ui/nowplaying/NowPlayingFragment.kt View File

@@ -49,12 +49,12 @@ class NowPlayingFragment : Fragment() {
49 49
         val volumeText: TextView = root.findViewById(R.id.volume_text)
50 50
         val progressBar: ProgressBar = root.findViewById(R.id.progressBar)
51 51
         val volumeIconImage : ImageView = root.findViewById(R.id.volume_icon)
52
-
52
+        val currentProgrammeText: TextView  = root.findViewById(R.id.text_current_programme)
53 53
         val streamerPictureImageView: ImageView = root.findViewById(R.id.streamerPicture)
54 54
 
55 55
         // Note: these values are not used in the generic app, but if you want to, you can use them.
56 56
         val songTitleNextText: TextView = root.findViewById(R.id.text_song_title_next)
57
-        //val songArtistNextText: TextView = root.findViewById(R.id.text_song_artist_next)
57
+        val songArtistNextText: TextView = root.findViewById(R.id.text_song_artist_next)
58 58
 
59 59
         /*
60 60
         val streamerNameText : TextView = root.findViewById(R.id.streamerName)
@@ -69,15 +69,15 @@ class NowPlayingFragment : Fragment() {
69 69
             listenersText,8, 16, 2, TypedValue.COMPLEX_UNIT_SP)
70 70
          */
71 71
 
72
-        /*
72
+
73 73
         // trick : I can't observe the queue because it's an ArrayDeque that doesn't trigger any change...
74 74
         // so I observe a dedicated Mutable that gets set when the queue is updated.
75 75
         PlayerStore.instance.isQueueUpdated.observe(viewLifecycleOwner, Observer {
76
-            val t = if (PlayerStore.instance.queue.size > 0) PlayerStore.instance.queue[0] else Song("No queue - ") // (it.peekFirst != null ? it.peekFirst : Song() )
76
+            val t = if (PlayerStore.instance.queue.size > 0) PlayerStore.instance.queue[0] else Song(noConnectionValue) // (it.peekFirst != null ? it.peekFirst : Song() )
77 77
             songTitleNextText.text = t.title.value
78 78
             songArtistNextText.text = t.artist.value
79 79
         })
80
-
80
+        /*
81 81
         PlayerStore.instance.streamerName.observe(viewLifecycleOwner, Observer {
82 82
             streamerNameText.text = it
83 83
         })
@@ -92,10 +92,12 @@ class NowPlayingFragment : Fragment() {
92 92
             songTitleText.text = it
93 93
         })
94 94
 
95
+
95 96
         Planning.instance.currentProgramme.observe(viewLifecycleOwner, Observer {
96
-            songTitleNextText.text = it
97
+            currentProgrammeText.text = it
97 98
         })
98 99
 
100
+
99 101
         PlayerStore.instance.currentSong.artist.observe(viewLifecycleOwner, Observer {
100 102
             songArtistText.text = it
101 103
         })
@@ -273,7 +275,7 @@ class NowPlayingFragment : Fragment() {
273 275
                 (viewHeight*100)/viewWidth
274 276
         else
275 277
             100
276
-        //[REMOVE LOG CALLS]//[REMOVE LOG CALLS]Log.d(tag, "orientation set")
278
+        //[REMOVE LOG CALLS]Log.d(tag, "orientation set")
277 279
     }
278 280
 
279 281
     override fun onResume() {

+ 2 - 2
app/src/main/java/fr/forum_thalie/tsumugi/ui/programme/ProgrammeFragment.kt View File

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

+ 1 - 1
app/src/main/java/fr/forum_thalie/tsumugi/ui/songs/SongsFragment.kt View File

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

+ 1 - 1
app/src/main/java/fr/forum_thalie/tsumugi/ui/songs/queuelp/LastPlayedFragment.kt View File

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

+ 49 - 19
app/src/main/res/layout/fragment_nowplaying.xml View File

@@ -93,13 +93,6 @@
93 93
             android:visibility="gone"
94 94
             />
95 95
 
96
-        <androidx.constraintlayout.widget.Guideline
97
-            android:id="@+id/topInfoGuideline"
98
-            android:layout_width="wrap_content"
99
-            android:layout_height="wrap_content"
100
-            android:orientation="horizontal"
101
-            app:layout_constraintGuide_percent="0.95" />
102
-
103 96
         <TextView
104 97
             android:id="@+id/sleepInfo"
105 98
             android:layout_width="0dp"
@@ -162,7 +155,8 @@
162 155
             android:layout_width="0dp"
163 156
             android:layout_height="0dp"
164 157
             android:fillViewport="true"
165
-            app:layout_constraintBottom_toTopOf="@id/topInfoGuideline"
158
+            android:layout_marginBottom="4dp"
159
+            app:layout_constraintBottom_toTopOf="@id/text_current_programme"
166 160
             app:layout_constraintEnd_toStartOf="@id/streamerPicture"
167 161
             app:layout_constraintStart_toStartOf="parent"
168 162
             app:layout_constraintTop_toBottomOf="@id/seek_bar_volume">
@@ -184,7 +178,7 @@
184 178
                     android:layout_gravity="top"
185 179
                     android:text="@string/up_next"
186 180
                     android:textAlignment="center"
187
-                    android:textColor="@color/whited3"
181
+                    android:textColor="@color/whited5"
188 182
                     android:layout_marginTop="8dp"
189 183
                     app:layout_constraintStart_toStartOf="parent"
190 184
                     app:layout_constraintEnd_toEndOf="parent"
@@ -199,10 +193,10 @@
199 193
                     android:text=""
200 194
                     android:textAlignment="center"
201 195
                     android:textColor="@color/whited3"
202
-                    android:textSize="16sp"
196
+                    android:textSize="14sp"
203 197
                     app:layout_constraintStart_toStartOf="parent"
204 198
                     app:layout_constraintTop_toBottomOf="@id/upNext"
205
-                    android:visibility="gone"/>
199
+                    android:visibility="visible"/>
206 200
 
207 201
                 <TextView
208 202
                     android:id="@+id/text_song_title_next"
@@ -212,14 +206,50 @@
212 206
                     android:text=""
213 207
                     android:textAlignment="center"
214 208
                     android:textColor="@color/whited"
215
-                    android:textSize="16sp"
209
+                    android:textSize="14sp"
216 210
                     app:layout_constraintStart_toStartOf="parent"
217
-                    app:layout_constraintTop_toBottomOf="@id/text_song_artist_next" />
211
+                    app:layout_constraintTop_toBottomOf="@id/text_song_artist_next"
212
+                    android:visibility="visible" />
218 213
 
219 214
             </androidx.constraintlayout.widget.ConstraintLayout>
220 215
         </ScrollView>
221 216
 
217
+        <TextView
218
+            android:id="@+id/current_programme"
219
+            android:layout_width="wrap_content"
220
+            android:layout_height="wrap_content"
221
+            android:gravity="bottom"
222
+            android:text="@string/current_programme"
223
+            android:textAlignment="center"
224
+            android:textColor="@color/whited3"
225
+            android:textSize="14sp"
226
+            app:layout_constraintStart_toStartOf="parent"
227
+            app:layout_constraintBottom_toBottomOf="@id/topInfoGuideline"
228
+            android:visibility="visible" />
229
+
230
+        <TextView
231
+            android:id="@+id/text_current_programme"
232
+            android:layout_width="0dp"
233
+            android:layout_height="wrap_content"
234
+            android:text=""
235
+            android:layout_marginStart="8sp"
236
+            android:layout_marginLeft="8sp"
237
+            android:gravity="start|bottom"
238
+            android:textAlignment="textStart"
239
+            android:textColor="@color/whited"
240
+            android:textSize="14sp"
241
+            app:layout_constraintStart_toEndOf="@id/current_programme"
242
+            app:layout_constraintEnd_toEndOf="parent"
243
+            app:layout_constraintBottom_toBottomOf="@id/topInfoGuideline"
244
+            android:visibility="visible"
245
+             />
222 246
 
247
+        <androidx.constraintlayout.widget.Guideline
248
+            android:id="@+id/topInfoGuideline"
249
+            android:layout_width="wrap_content"
250
+            android:layout_height="wrap_content"
251
+            android:orientation="horizontal"
252
+            app:layout_constraintGuide_percent="0.97" />
223 253
 
224 254
     </androidx.constraintlayout.widget.ConstraintLayout>
225 255
 
@@ -268,7 +298,7 @@
268 298
                     android:text=""
269 299
                     android:textAlignment="center"
270 300
                     android:textColor="@color/whited"
271
-                    android:textSize="20sp"
301
+                    android:textSize="18sp"
272 302
                     app:layout_constraintBottom_toBottomOf="parent"
273 303
                     app:layout_constraintStart_toStartOf="parent" />
274 304
 
@@ -280,7 +310,7 @@
280 310
                     android:text=""
281 311
                     android:textAlignment="center"
282 312
                     android:textColor="@color/whited3"
283
-                    android:textSize="20sp"
313
+                    android:textSize="18sp"
284 314
                     app:layout_constraintBottom_toTopOf="@id/text_song_title"
285 315
                     app:layout_constraintStart_toStartOf="parent" />
286 316
 
@@ -305,7 +335,7 @@
305 335
             android:progressDrawable="@drawable/progress_bar_progress"
306 336
             app:layout_constraintBottom_toTopOf="@id/play_pause"
307 337
             tools:layout_editor_absoluteX="0dp"
308
-            android:visibility="gone"/>
338
+            android:visibility="visible"/>
309 339
 
310 340
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
311 341
         <TextView
@@ -318,7 +348,7 @@
318 348
             android:textAlignment="textEnd"
319 349
             app:layout_constraintEnd_toEndOf="@id/progressBar"
320 350
             app:layout_constraintTop_toBottomOf="@id/progressBar"
321
-            android:visibility="gone"/>
351
+            android:visibility="visible"/>
322 352
 
323 353
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
324 354
         <TextView
@@ -331,7 +361,7 @@
331 361
             android:textAlignment="textStart"
332 362
             app:layout_constraintStart_toStartOf="@id/progressBar"
333 363
             app:layout_constraintTop_toBottomOf="@id/progressBar"
334
-            android:visibility="gone"/>
364
+            android:visibility="visible"/>
335 365
 
336 366
 
337 367
 
@@ -381,7 +411,7 @@
381 411
         android:layout_width="wrap_content"
382 412
         android:layout_height="wrap_content"
383 413
         android:orientation="horizontal"
384
-        app:layout_constraintGuide_percent="0.33" />
414
+        app:layout_constraintGuide_percent="0.38" />
385 415
 
386 416
     <androidx.constraintlayout.widget.Guideline
387 417
     android:id="@+id/splitHorizontalLayout"

+ 31 - 0
app/src/main/res/values-fr/arrays.xml View File

@@ -0,0 +1,31 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<resources>
3
+
4
+    <string-array name="snoozeValues">
5
+        <item name="0">@string/disable</item>
6
+        <item name="1">1</item>
7
+        <item name="2">2</item>
8
+        <item name="5">5</item>
9
+        <item name="10">10</item>
10
+        <item name="15">15</item>
11
+        <item name="20">20</item>
12
+        <item name="25">25</item>
13
+        <item name="30">30</item>
14
+    </string-array>
15
+
16
+    <string-array name="fetchPeriodString">
17
+        <item name="5">Toutes les 5 secondes (mise à jour rapide)</item>
18
+        <item name="10">Toutes les 10 secondes</item>
19
+        <item name="15">Toutes les 15 secondes</item>
20
+        <item name="20">Toutes les 20 secondes</item>
21
+        <item name="30">Toutes les 30 secondes (moins d\'utilisation de batterie)</item>
22
+    </string-array>
23
+    <string-array name="fetchPeriodValues">
24
+        <item name="5">5</item>
25
+        <item name="10">10</item>
26
+        <item name="15">15</item>
27
+        <item name="20">20</item>
28
+        <item name="30">30</item>
29
+    </string-array>
30
+
31
+</resources>

+ 3 - 1
app/src/main/res/values-fr/strings.xml View File

@@ -10,7 +10,8 @@
10 10
 
11 11
     <string name="volume">Volume : </string>
12 12
 
13
-    <string name="up_next">Émission en cours :</string>
13
+    <string name="up_next">Prochain titre :</string>
14
+    <string name="current_programme">Émission en cours : </string>
14 15
     <string name="now_streaming">En cours</string>
15 16
     <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>
16 17
     <string name="action_settings">Paramètres</string>
@@ -50,5 +51,6 @@
50 51
     <string name="sleepClosesApp">Minuterie avant fermeture de l\'application</string>
51 52
     <string name="setSleepDuration">Choisir une durée (en minutes)</string>
52 53
     <string name="willCloseIn">Extinction dans %1$d minutes</string>
54
+    <string name="fetchPeriod">Choisir la fréquence de mise à jour quand la radio est stoppée</string>
53 55
 
54 56
 </resources>

+ 15 - 0
app/src/main/res/values/arrays.xml View File

@@ -13,4 +13,19 @@
13 13
         <item name="30">30</item>
14 14
     </string-array>
15 15
 
16
+    <string-array name="fetchPeriodString">
17
+        <item name="5">Every 5 seconds (faster update)</item>
18
+        <item name="10">Every 10 seconds</item>
19
+        <item name="15">Every 15 seconds</item>
20
+        <item name="20">Every 20 seconds</item>
21
+        <item name="30">Every 30 seconds (less battery-intensive)</item>
22
+    </string-array>
23
+    <string-array name="fetchPeriodValues">
24
+        <item name="5">5</item>
25
+        <item name="10">10</item>
26
+        <item name="15">15</item>
27
+        <item name="20">20</item>
28
+        <item name="30">30</item>
29
+    </string-array>
30
+
16 31
 </resources>

+ 3 - 3
app/src/main/res/values/colors.xml View File

@@ -11,7 +11,7 @@
11 11
     <color name="whited3">#999999</color>
12 12
     <color name="whited4">#999999</color>
13 13
     <color name="whited5">#7F7F7F</color>
14
-    <color name="rblue">#4FC3F7</color>
14
+    <color name="rblue">#64B5F6</color>
15 15
     <color name="bluereq">#527a8e</color>
16 16
     <color name="blueTitle">#527a8e</color>
17 17
     <color name="reqButtonPressed">#5cb85c</color>
@@ -20,10 +20,10 @@
20 20
 
21 21
     <color name="seek_bar_background">#585858</color>
22 22
     <color name="seek_bar_progress">#f58b01</color>
23
-    <color name="seek_bar_secondary_progress">#4FC3F7</color>
23
+    <color name="seek_bar_secondary_progress">#64B5F6</color>
24 24
 
25 25
     <color name="progress_bar_background">#585858</color>
26
-    <color name="progress_bar_progress">#4FC3F7</color>
26
+    <color name="progress_bar_progress">#64B5F6</color>
27 27
     <color name="progress_bar_secondary_progress">#f58b01</color>
28 28
 
29 29
     <color name="white">#FFFFFF</color>

+ 4 - 1
app/src/main/res/values/strings.xml View File

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

+ 10 - 0
app/src/main/res/xml/customize_preferences.xml View File

@@ -29,4 +29,14 @@
29 29
         app:defaultValue="false"
30 30
         />
31 31
 
32
+    <ListPreference
33
+        app:key="fetchPeriod"
34
+        app:iconSpaceReserved="false"
35
+        android:title="@string/fetchPeriod"
36
+        app:singleLineTitle="false"
37
+        android:entries="@array/fetchPeriodString"
38
+        android:entryValues="@array/fetchPeriodValues"
39
+        android:defaultValue="10"
40
+        />
41
+
32 42
 </PreferenceScreen>