Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494364ffa2 | ||
|
|
80a56c01c9 |
No files matched your search
@@ -90,7 +90,15 @@
|
||||
<string name="flexera_update_check_idle">Flexera Launcher also checks on its own, once a day</string>
|
||||
<string name="flexera_update_checking">Checking for updates…</string>
|
||||
<string name="flexera_update_up_to_date">You have the latest version</string>
|
||||
<string name="flexera_update_check_failed">Couldn\'t reach the update server. Check your connection, then tap to try again.</string>
|
||||
<!-- Why a check for updates failed. %1$s is the update server's name, such as git.example.org -->
|
||||
<string name="flexera_update_failed_no_permission">Android does not let Flexera Launcher use the network on this phone. Allow it in the phone\'s settings for this app, under Permissions, Network, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_not_found">This phone could not find %1$s. Check the connection and any VPN or private DNS, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_no_answer">%1$s did not answer. Check the connection and any VPN, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_not_secure">A secure connection to %1$s could not be made. Check the phone\'s date and time and any VPN, then tap to try again.</string>
|
||||
<string name="flexera_update_failed_server_error">%1$s answered with an error. Tap to try again later.</string>
|
||||
<string name="flexera_update_failed_bad_answer">The answer from %1$s could not be read. Tap to try again later.</string>
|
||||
<!-- %1$s is the error in the system\'s own words, for whoever looks for the cause -->
|
||||
<string name="flexera_update_failed_detail">Details: %1$s</string>
|
||||
<!-- %1$s is a version number such as 0.2.0 -->
|
||||
<string name="flexera_update_download">Download version %1$s</string>
|
||||
<!-- %1$s is a file size such as 24 MB -->
|
||||
|
||||
@@ -47,6 +47,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.flexera.update.CheckProblem
|
||||
import app.flexera.update.FlexeraRelease
|
||||
import app.flexera.update.FlexeraUpdater
|
||||
import app.flexera.update.UpdateFailure
|
||||
@@ -294,7 +295,7 @@ private fun UpdateStatusRow(state: UpdateState) {
|
||||
if (release == null) {
|
||||
ClickablePreference(
|
||||
label = stringResource(id = R.string.flexera_update_check),
|
||||
subtitle = stringResource(id = R.string.flexera_update_check_failed),
|
||||
subtitle = checkFailureText(state),
|
||||
onClick = { FlexeraUpdater.check(context) },
|
||||
)
|
||||
} else {
|
||||
@@ -379,6 +380,25 @@ private fun QrCode(text: String, description: String, modifier: Modifier = Modif
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Says why a check for updates failed and what to do about it, then the error's own words, which
|
||||
* are what someone looking for the cause will ask for.
|
||||
*/
|
||||
@Composable
|
||||
private fun checkFailureText(state: UpdateState.Failed): String {
|
||||
val host = FlexeraUpdater.SERVER_HOST
|
||||
val reason = when (state.problem) {
|
||||
CheckProblem.NO_PERMISSION -> stringResource(id = R.string.flexera_update_failed_no_permission)
|
||||
CheckProblem.SERVER_NOT_FOUND -> stringResource(id = R.string.flexera_update_failed_not_found, host)
|
||||
CheckProblem.NOT_SECURE -> stringResource(id = R.string.flexera_update_failed_not_secure, host)
|
||||
CheckProblem.SERVER_ERROR -> stringResource(id = R.string.flexera_update_failed_server_error, host)
|
||||
CheckProblem.BAD_ANSWER -> stringResource(id = R.string.flexera_update_failed_bad_answer, host)
|
||||
CheckProblem.NO_ANSWER, null -> stringResource(id = R.string.flexera_update_failed_no_answer, host)
|
||||
}
|
||||
val detail = state.detail ?: return reason
|
||||
return reason + "\n" + stringResource(id = R.string.flexera_update_failed_detail, detail)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun downloadSizeText(context: Context, release: FlexeraRelease): String =
|
||||
if (release.apkSize > 0) {
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
import javax.net.ssl.SSLException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -31,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
/** One published version of Flexera Launcher. */
|
||||
@@ -56,6 +59,31 @@ enum class UpdateFailure {
|
||||
WRONG_FILE,
|
||||
}
|
||||
|
||||
/**
|
||||
* Why a check for updates failed, as far as the phone can tell. The About screen says it in
|
||||
* words, because "could not reach the server" alone leaves the owner guessing between the
|
||||
* connection, a VPN, the server and the phone's own settings.
|
||||
*/
|
||||
enum class CheckProblem {
|
||||
/** Android does not let this app use the network (a per-app switch on some systems). */
|
||||
NO_PERMISSION,
|
||||
|
||||
/** The server's name could not be looked up. */
|
||||
SERVER_NOT_FOUND,
|
||||
|
||||
/** The server was found but did not answer in time, or refused the connection. */
|
||||
NO_ANSWER,
|
||||
|
||||
/** The encrypted connection could not be set up. */
|
||||
NOT_SECURE,
|
||||
|
||||
/** The server answered, with an error. */
|
||||
SERVER_ERROR,
|
||||
|
||||
/** The server answered with something that is not a release. */
|
||||
BAD_ANSWER,
|
||||
}
|
||||
|
||||
/** Where the updater currently stands. The About screen shows one row per state. */
|
||||
sealed interface UpdateState {
|
||||
/** Nothing has been checked since the launcher started. */
|
||||
@@ -72,8 +100,17 @@ sealed interface UpdateState {
|
||||
|
||||
data class ReadyToInstall(val release: FlexeraRelease, val file: File) : UpdateState
|
||||
|
||||
/** [release] is set when the failure happened after a newer version had been found. */
|
||||
data class Failed(val failure: UpdateFailure, val release: FlexeraRelease? = null) : UpdateState
|
||||
/**
|
||||
* [release] is set when the failure happened after a newer version had been found. For a
|
||||
* failed check, [problem] says why and [detail] carries the error's own words, for
|
||||
* whoever has to find the cause.
|
||||
*/
|
||||
data class Failed(
|
||||
val failure: UpdateFailure,
|
||||
val release: FlexeraRelease? = null,
|
||||
val problem: CheckProblem? = null,
|
||||
val detail: String? = null,
|
||||
) : UpdateState
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +227,7 @@ object FlexeraUpdater {
|
||||
Log.w(TAG, "Update check failed", error)
|
||||
// A background check that fails stays quiet: the About screen keeps what it showed
|
||||
// before, including an update that is already waiting. It tries again in an hour.
|
||||
mutableState.value = if (automatic) before else UpdateState.Failed(UpdateFailure.CHECK)
|
||||
mutableState.value = if (automatic) before else failedCheck(error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -302,13 +339,41 @@ object FlexeraUpdater {
|
||||
private fun isBusy(): Boolean =
|
||||
mutableState.value.let { it is UpdateState.Checking || it is UpdateState.Downloading }
|
||||
|
||||
/** The server's name without the scheme, for messages. */
|
||||
@JvmField
|
||||
val SERVER_HOST: String = SERVER.substringAfter("://")
|
||||
|
||||
private class NoNetworkPermissionException : IOException("This app has no network permission")
|
||||
private class ServerErrorException(status: Int) : IOException("The server answered HTTP $status")
|
||||
private class BadAnswerException(message: String) : IOException(message)
|
||||
|
||||
/** Turns the error of a failed check into a state that says what kind of failure it was. */
|
||||
private fun failedCheck(error: Throwable): UpdateState.Failed {
|
||||
val problem = when (error) {
|
||||
is NoNetworkPermissionException, is SecurityException -> CheckProblem.NO_PERMISSION
|
||||
is UnknownHostException -> CheckProblem.SERVER_NOT_FOUND
|
||||
is SSLException -> CheckProblem.NOT_SECURE
|
||||
is ServerErrorException -> CheckProblem.SERVER_ERROR
|
||||
is BadAnswerException, is JSONException -> CheckProblem.BAD_ANSWER
|
||||
else -> CheckProblem.NO_ANSWER
|
||||
}
|
||||
// Android's own error classes keep their names in a release build; this file's are
|
||||
// renamed by the shrinker, and their messages say everything anyway.
|
||||
val name = error.javaClass.name.takeIf { it.startsWith("java") || it.startsWith("android") || it.startsWith("org.json") }
|
||||
?.substringAfterLast('.')
|
||||
val detail = listOfNotNull(name, error.message).joinToString(": ").take(MAX_DETAIL_LENGTH)
|
||||
return UpdateState.Failed(UpdateFailure.CHECK, problem = problem, detail = detail.ifEmpty { null })
|
||||
}
|
||||
|
||||
private const val MAX_DETAIL_LENGTH = 200
|
||||
|
||||
/** Returns the latest published release, or null when nothing has been published yet. */
|
||||
private fun fetchLatestRelease(context: Context): FlexeraRelease? {
|
||||
val connection = open(context, LATEST_RELEASE_URL)
|
||||
try {
|
||||
val status = connection.responseCode
|
||||
if (status == HttpURLConnection.HTTP_NOT_FOUND) return null
|
||||
if (status != HttpURLConnection.HTTP_OK) throw IOException("Update server answered HTTP $status")
|
||||
if (status != HttpURLConnection.HTTP_OK) throw ServerErrorException(status)
|
||||
|
||||
val json = JSONObject(connection.inputStream.bufferedReader().use { it.readText() })
|
||||
val tag = json.getString("tag_name")
|
||||
@@ -326,7 +391,7 @@ object FlexeraUpdater {
|
||||
apkSize = asset.optLong("size", 0L),
|
||||
)
|
||||
}
|
||||
throw IOException("Release $tag has no APK attached")
|
||||
throw BadAnswerException("Release $tag has no APK attached")
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
@@ -373,7 +438,7 @@ object FlexeraUpdater {
|
||||
}
|
||||
|
||||
private fun open(context: Context, url: String): HttpURLConnection {
|
||||
if (!FlexeraNetwork.hasInternetPermission(context)) throw IOException("No INTERNET permission")
|
||||
if (!FlexeraNetwork.hasInternetPermission(context)) throw NoNetworkPermissionException()
|
||||
return (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = CONNECT_TIMEOUT_MS
|
||||
readTimeout = READ_TIMEOUT_MS
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# build.gradle reads this and derives the Android version code from it
|
||||
# (major * 10000 + minor * 100 + patch), so the name and the code can never disagree.
|
||||
# flexera/release.ps1 raises it for every release.
|
||||
versionName=0.6.0
|
||||
versionName=0.6.1
|
||||
Reference in new issue
Block a user