Commit 9cd2c754 authored by Daniel Sonck's avatar Daniel Sonck
Browse files

[THFMA-30] [THFMA-31] [THFMA-32] Version checking

- Updated translations
- Added changelog window
- Prevent launching a deprecated version
- Show update dialog once when new version is released
parent 4625f19c
......@@ -62,6 +62,9 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
implementation ''
implementation project(':changelog')
// The sample build uses multiple directories to
......@@ -74,11 +77,11 @@ List<String> dirs = [
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
minSdkVersion 14
targetSdkVersion 28
targetSdkVersion 29
applicationId "fm.touhou.touhoufm"
versionCode gitVersionCode
......@@ -35,9 +35,16 @@
package fm.touhou.touhoufm.ui
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.widget.Toolbar
import androidx.drawerlayout.widget.DrawerLayout
......@@ -45,24 +52,39 @@ import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.*
import ch.derlin.changelog.Changelog
import fm.touhou.touhoufm.BuildConfig
import fm.touhou.touhoufm.R
import org.json.JSONObject
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration : AppBarConfiguration
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var queue: RequestQueue
override fun onCreate(savedInstanceState: Bundle?) {
queue = Volley.newRequestQueue(this)
val prefs = getSharedPreferences("AppDetails", Context.MODE_PRIVATE);
val toolbar = findViewById<Toolbar>(
val host: NavHostFragment = supportFragmentManager.findFragmentById( as NavHostFragment? ?: return
val host: NavHostFragment = supportFragmentManager.findFragmentById( as NavHostFragment?
?: return
val navController = host.navController
val drawerLayout : DrawerLayout? = findViewById(
val drawerLayout: DrawerLayout? = findViewById(
appBarConfiguration = AppBarConfiguration(
......@@ -70,8 +92,77 @@ class MainActivity : AppCompatActivity() {
setupActionBar(navController, appBarConfiguration)
val appVersion = BuildConfig.VERSION_CODE
val lastVersion = 42//prefs.getInt("LatestVersion", 0)
val lastKnownVersion = prefs.getInt("LatestReleaseVersion", 0)
val jsonRequest = JsonObjectRequest("", null,
Response.Listener { response: JSONObject ->
val supported: Boolean;
when {
appVersion < response.getInt("supported") -> {
supported = false;
.setTitle("Unsupported app version")
.setMessage("This version of the app is currently unsupported, please download the new version from the play store")
.setPositiveButton("Download", DialogInterface.OnClickListener { dialogInterface, i ->
val appPackageName = packageName
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName")))
} catch (anfe: android.content.ActivityNotFoundException) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("$appPackageName")))
.setNegativeButton("Quit", DialogInterface.OnClickListener { dialogInterface, i ->
appVersion < response.getInt("beta") - 1 -> {
supported = true;
if (lastVersion != appVersion || lastKnownVersion != response.getInt("beta") - 1) {
.setTitle("New version")
.setMessage("A new version is available in the play store")
.setPositiveButton("Download", DialogInterface.OnClickListener { dialogInterface, i ->
val appPackageName = packageName
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName")))
} catch (anfe: android.content.ActivityNotFoundException) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("$appPackageName")))
.setNegativeButton(android.R.string.cancel, null)
appVersion < response.getInt("alpha") -> {
supported = true;
Toast.makeText(this, "Thanks for being a beta tester", LENGTH_LONG).show()
else -> {
supported = true;
Toast.makeText(this, "Thanks for being an alpha tester", LENGTH_LONG).show()
if (supported && lastVersion != appVersion) {
prefs.edit().putInt("LatestVersion", appVersion).apply()
Changelog.createDialog(this, versionCode = lastVersion).show()
}, Response.ErrorListener {
Toast.makeText(this, "Failed to check latest version", Toast.LENGTH_LONG).show();
private fun setupNavigationMenu(navController: NavController) {
val sideNavView = findViewById<NavigationView>(
......@@ -80,7 +171,7 @@ class MainActivity : AppCompatActivity() {
private fun setupActionBar(navController: NavController,
appBarConfig : AppBarConfiguration) {
appBarConfig: AppBarConfiguration) {
setupActionBarWithNavController(navController, appBarConfig)
This application tunes into the TouHou.FM radio. This radio streams original and doujin music from Touhou Project by TeamShanghaiAlice.
Zurzeit ist es eine Alpha-Version, in welcher nicht Alles implementiert ist.
Unter anderem lassen sich auch einige Bugs bzw. Fehler finden.
Ich bitte Sie eine Nachricht für alle Fehler, Anregungen, positives oder negatives Feedback, an zu senden.
\ No newline at end of file
Hören Sie TouHow.FM direkt von ihrem Handy, Tablet oder allen anderen Geräten.
\ No newline at end of file
Radio Spieler von TouHou.FM
\ No newline at end of file
Fixed Metadatei informationen
Stabiele weiterentwicklung
Widget zugefügt
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
* Copyright 2018 Daniel Sonck, translation Patrick Büchele
* This file is part of fm.touhou.touhoufm.
* fm.touhou.touhoufm is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, version 2.
* fm.touhou.touhoufm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with fm.touhou.touhoufm. If not, see <>.
<string name="app_name">TouHou.FM</string>
<string name="label_stop">Stop</string>
<string name="label_pause">Pause</string>
<string name="label_play">Abspielen</string>
<string name="label_play_pause">Pause und Abspielen umschalten</string>
<string name="time_format">%2$d:%1$02d</string><!-- MM:SS -->
<string name="album_format">( %1$s )</string><!-- ( <album name> ) -->
<string name="artist_circle_format">%1$s ( %2$s )</string><!-- <artist name> ( <circle name> ) -->
<string name="unknown_song">Unbekantes Lied</string>
<string name="unknown_album">Unbekantes Album</string>
<string name="unknown_artist">Umbekanter Künstler</string>
<string name="unknown_circle">Unbekanter Kreise</string>
<string name="home">Home</string>
<string name="settings">Einstellungen</string>
<string name="stream_port_summary">Welchen Port benutzen fürs streaming</string>
<string name="stream_port_title">Streaming Port</string>
<string name="stream_custom_title">Benutze benutzerdefinierten stream server</string>
<string name="stream_host_summary">Welche Adresse fürs Streaming benutzt wird</string>
<string name="stream_host_title">Streaming Host</string>
<string name="category_advanced_summary">Einstellungen für Power User</string>
<string name="category_advanced_title">Fortgeschritten</string>
<string name="keep_metadata_alive_summary">Lass die Verbindung offen fürs erhalten von Metadata Updates. Es braucht Daten, auch wenn der Vorgang gestoppt ist.</string>
<string name="keep_metadata_alive_title">Immer Fordern nach metadata</string>
<string name="show_data_summary">Zeigt den Datenverbrauch vom streaming</string>
<string name="show_data_title">Datenverbrauch</string>
<string name="streaming_rate_title">Streaming Bitrate</string>
<string name="streaming_rate_summary">Maximal verfügbare Bitrate fürs Streaming</string>
<string name="category_streaming_title">Streaming</string>
<string name="category_streaming_summary">Feinjustieren für die Streamingqualität und den Datenverbrauch</string>
\ No newline at end of file
......@@ -40,5 +40,6 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="changelogStyle">@style/LibChangelog</item>
<?xml version="1.0" encoding="utf-8"?>
<changelog >
<release version="v2.0.19" versioncode="44">
<change>Updated translations, added German</change>
<change>Add this changelog window</change>
<change>Notify users when version is outdated, prevent launch when unsupported</change>
<change>Fix crash on pause</change>
<change>Show song progress information</change>
<change>Expert: Add ability to pick a streaming server</change>
<change>Widget support</change>
<change>Update icon to allow launchers to use their style</change>
<release version="v2.0.18" versioncode="33">
<change>Update to new streaming specifications</change>
<change>Song info works again</change>
<change>Fixed code causing crashes</change>
<release version="v2.0.13" versioncode="27">
<change>Updated translations</change>
<change>Fix memoryleak</change>
<release version="v2.0.12" versioncode="24">
<change>Fix crash on startup</change>
<release version="v2.0.11" versioncode="23">
<change>THFMA-8: Using app Quit will remove notification as well</change>
<change>THFMA-9: Add information and playback buttons to the notification</change>
<change>THFMA-10: Fix app not stopping playback immediately</change>
<change>THFMA-11: Fix music sounding slow after switch from Data to Wifi</change>
<change>THFMA-12: Integrate better with android. App will advertise it's playing and automatically stop if another source is starting (e.g. youtube)</change>
<release version="v2.0.10" versioncode="22">
<change>Small compatibility fixes</change>
<release version="v2.0.9" versioncode="21">
<change>Improved stability</change>
<release version="v2.0.8" versioncode="20">
<change>Fixed endless asking for power management</change>
<release version="v2.0.7" versioncode="19">
<change>Fix battery optimization detection</change>
<release version="v2.0.6" versioncode="18">
<change>Added dialog to disable optimisations for the app which can cause random issues during streaming while screen is off</change>
<change>Updated translations</change>
<release version="v2.0.5" versioncode="17">
<change>Fixed crashing bug</change>
<release version="v2.0.4" versioncode="16">
<change>Added translations: French and Hungarian</change>
<change>Fixed bug causing crash when no network is available</change>
<release version="v2.0.3" versioncode="15">
<change>Removed failing login UI</change>
<change>Removed unused settings screen</change>
<release version="v2.0.0" versioncode="12">
<change>New streaming technology improving reliability and quality especially on roaming networks</change>
\ No newline at end of file
......@@ -4,7 +4,7 @@
......@@ -27,12 +27,7 @@
android:title="@string/stream_custom_title" />
......@@ -49,5 +44,9 @@
app:useSimpleSummaryProvider="true" />
android:title="@string/stream_custom_title" />
\ No newline at end of file
......@@ -23,9 +23,9 @@ import android.util.Log
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.*
......@@ -33,20 +33,16 @@ import java.util.*
class RingBufferTest {
internal inner class Reader(private val buff: RingBuffer, size: Int) : Thread() {
var out: ShortArray
var out: ShortArray = ShortArray(size)
var readReturn: Int = 0
init {
out = ShortArray(size)
override fun run() {
readReturn =, out.size)
fun setupMocks() {
every { Log.v(any(), any()) } returns 0
apply plugin: ''
apply plugin: 'kotlin-android'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 14
targetSdkVersion 28
versionCode 2
versionName "2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), ''
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha04'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation ""
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0-alpha03'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-alpha03'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
repositories {
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# For more details, see
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
package ch.derlin.changelog;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
* Instrumented test, which will execute on an Android device.
* @see <a href="">Testing documentation</a>
public class ExampleInstrumentedTest {
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("ch.derlin.changelog.test", appContext.getPackageName());
<manifest xmlns:android=""
package="ch.derlin.changelog" />
package ch.derlin.changelog
import android.content.Context
import android.content.res.XmlResourceParser
import android.text.format.DateFormat
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.sql.Date
import java.text.ParseException
* Changelog
* Copyright (C) 2018 Lucy Linder (derlin)
* This software may be modified and distributed under the terms
* of the Apache 2.0 license. See the LICENSE file for details.
* Updated to AndroidX: Copyright (C) 2019 Arne Rantzen (Tyxz)
object Changelog {
/** Use this value if you want all the changelog (i.e. all the release entries) to appear. */
/** Constants for xml tags and attributes (see res/xml/changelog.xml for an example) */
object XmlTags {
val RELEASE = "release"
val ITEM = "change"
val VERSION_NAME = "version"
val VERSION_CODE = "versioncode"
val SUMMARY = "summary"
val DATE = "date"
val TYPE ="type"
* Create a dialog displaying the changelog.
* @param ctx The calling activity
* @param versionCode Define the oldest version to show. In other words, the dialog will contains
* release entries with a `versionCode` attribute >= [versionCode]. Default to all.
* @param title The title of the dialog. Default to "Changelog"
* @param resId The resourceId of the xml file, default to `R.xml.changelog`
@Throws(XmlPullParserException::class, IOException::class)
fun createDialog(ctx: Activity, versionCode: Int = ALL_VERSIONS,
title: String? = null, resId: Int = R.xml.changelog): AlertDialog {
return AlertDialog.Builder(ctx)
.setView(createChangelogView(ctx, versionCode, title, resId))
.setPositiveButton("OK") { _, _ -> }
* Create a custom view with the changelog list.
* This is the view that is displayed in the dialog on a call to [createDialog].
* See [createDialog] for the parameters.
@Throws(XmlPullParserException::class, IOException::class)
fun createChangelogView(ctx: Activity, versionCode: Int = ALL_VERSIONS,
title: String? = null, resId: Int = R.xml.changelog): View {
val view = ctx.layoutInflater.inflate(R.layout.changelog, null)
val changelog = loadChangelog(ctx, resId, versionCode)
title?.let { view.findViewById<TextView>( = it }
view.findViewById<RecyclerView>( = ChangelogAdapter(changelog)
return view
* Extension function to retrieve the current version of the application from the package.
* @return a pair <versionName, versionCode> (as set in the build.gradle file). Example: <"1.1", 3>
fun Activity.getAppVersion(): Pair<Long, String> {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
return Pair(getLongVersionCode(packageInfo), packageInfo.versionName)
// -----------------------------------------
* Read the changelog.xml and create a list of [ChangelogItem] and [ChangelogHeader].
* @param context: the calling activity
* @param resourceId: the name of the changelog file, default to R.xml.changelog
* @param version: the lowest release to display (see [createDialog] for more details)
* @return the list of [ChangelogItem], in the order of the [resourceId] file (most to less recent)
@Throws(XmlPullParserException::class, IOException::class)
private fun loadChangelog(context: Activity, resourceId: Int = R.xml.changelog, version: Int = ALL_VERSIONS):
MutableList<ChangelogItem> {
val clList = mutableListOf<ChangelogItem>()
val xml = context.resources.getXml(resourceId)
try {
while (xml.eventType != XmlPullParser.END_DOCUMENT) {
if (xml.eventType == XmlPullParser.START_TAG && == XmlTags.RELEASE) {
val releaseVersion = Integer.parseInt(xml.getAttributeValue(null, XmlTags.VERSION_CODE))
if (releaseVersion >= version) {
clList.addAll(parseReleaseTag(context, xml))
} else
} else {
} finally {