Skip to content

fix: probe the local server address without switching onto it - #1098

Open
herrerad85 wants to merge 3 commits into
eddyizm:developmentfrom
herrerad85:fix/local-address-probe-on-network-change
Open

herrerad85 wants to merge 3 commits into
eddyizm:developmentfrom
herrerad85:fix/local-address-probe-on-network-change

Conversation

@herrerad85

Copy link
Copy Markdown
Contributor

What is broken

Tempus lets you set a local network address next to the public one, and it picks between them by switching onto the local address and pinging it there. Everything the app is doing in that moment is aimed at an address that may have no route, including the saved play queue it rebuilds at launch. Start the app away from home and the whole restored queue points at your home network, so play and next do nothing while shuffle all works. When the ping fails the app switches back and reloads every screen.

The local address is now tested on a connection of its own, while the app carries on using the address it is already on. A test that gets no answer changes nothing, and the screens are reloaded only when one succeeds. The queue restore waits for that first answer, so no link is built from an address nothing has tried. Logging in also records which of a server's two addresses is in use, which it did not do before, so a login no longer inherits the address the previous server left behind.

Tested

On a phone, both directions. At home every link in the restored queue is built on the local address. Away from home the local address times out, the app moves to the public one, the queue is built there and playback starts. Also tested with a long ping timeout. Unit tests cover the address matching.

MainActivity moved the in use address onto the local one to find out whether it
answered. Off that network it cannot, so every screen request went to an address
with no route, the restored play queue was built with stream URLs carrying that
address, and resetView repaired the screens by clearing the activity's whole
ViewModelStore. That ran on every foreground while the phone was away from the
local network.

probeLocalAddress pings a client pinned to the local address and moves the in use
address only once an answer comes back. On a network that cannot reach the local
server the probe is the only request that goes there, nothing else changes, and no
screen is torn down. Back on the local network the probe answers, the address
moves and the screens are built again.

A probe still outstanding is what stops a second one going out, so the switch
window is stamped only where the app acts on an answer. That window is stored in
preferences and outlives the activity while the probe does not, so stamping it at
send left an activity recreated before the answer unable to probe again.

While a probe is outstanding the public ping is not sent, and a probe that fails
asks the public address itself. They used to run at once, so either answer could
settle a question the other was still deciding.

The saved play queue is held until the outstanding pings answer, so no stream URL
is built from an address that has not been tested. On a cold start away from home
the saved address is the local one and every URL built from it was dead. Two paths
restore that queue, and the one in MediaManager now leaves it to the media
service. The wait is capped at the configured ping timeout plus a second, and a
service started with no activity behind it never waits.

SystemClient.ping takes its timeout from the address its own client points at
instead of the address in use, since a probe runs while the in use address is
still the public one.

LoginFragment writes in_use_server_address when the stored one belongs to another
server, so a login inherits no address from the server selected before it.

Tested: 78 unit tests on one flavor and 75 on the other, and on a phone at home
and on cellular in both directions.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 29023edd-4bf3-4d67-b70e-7b5f49e056cc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tvillega

Copy link
Copy Markdown
Contributor

@herrerad85 I had to create a ping function for the connection test button on LoginServerFragment.

private fun testConnection() {
binding.testButton.isEnabled = false
Toast.makeText(
context,
getString(R.string.la_server_toast_connection_testing),
Toast.LENGTH_SHORT
).show()
val serverUrl = serverList[selectedServerPosition].address
val username = serverList[selectedServerPosition].username
val password = serverList[selectedServerPosition].password
val clientName = "Tempus"
val apiVersion = "1.16.0"
val url: String
if (serverList[selectedServerPosition].isLowSecurity) {
url = "$serverUrl/rest/ping.view?u=$username&p=$password&v=$apiVersion&c=$clientName&f=json"
} else {
val salt = UUID.randomUUID().toString().substring(0, 6)
val token = StringUtil.tokenize(password + salt)
url = "$serverUrl/rest/ping.view?u=$username&t=$token&s=$salt&v=$apiVersion&c=$clientName&f=json"
}
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
binding.testButton.post {
binding.testButton.isEnabled = true
Toast.makeText(
context,
getString(R.string.la_server_toast_connection_error) + e.localizedMessage,
Toast.LENGTH_LONG
).show()
}
}
override fun onResponse(call: Call, response: Response) {
response.use {
var isSubsonicOk = false
if (response.isSuccessful) {
val responseBody = response.body?.string()
if (responseBody != null) {
try {
val jsonRoot = JSONObject(responseBody)
val subsonicResponse =
jsonRoot.getJSONObject("subsonic-response")
if (subsonicResponse.getString("status") == "ok") {
isSubsonicOk = true
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// Switch back to Main Thread to update UI
binding.testButton.post {
binding.testButton.isEnabled = true
if (isSubsonicOk) {
Toast.makeText(
context,
getString(R.string.la_server_toast_connection_success),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
getString(R.string.la_server_toast_connection_failure),
Toast.LENGTH_LONG
).show()
}
}
}
}
})
}

Since you had to create functions in util/Preferences.kt, util/MusicUtil.java & MainActivity.java, I propose the creation of util/ConnectionUtil.kt to centralize everything which job is to "ping".

The connection test button on the login screen built its own request, with a
hand written URL, its own salt and token, a bare OkHttpClient and JSONObject
parsing. It now goes through SystemRepository.checkUserCredential, which takes
the client, and App.getSubsonicClientInstance builds one from a saved server
row without writing anything to the preferences.

So the test announces the same api version as the rest of the app and takes
the same ping timeout. A failure at the transport still says so, through the
callback's new onNetworkFailure.
@tvillega

Copy link
Copy Markdown
Contributor

Another feat that may be of relevance is #431

@herrerad85

Copy link
Copy Markdown
Contributor Author

Ohhh, this is helpful. Understood

Co-authored-by: Tom Villegas <tvillega@mailbox.org>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants