Add update important notice, download and install update from within the app

This commit is contained in:
Aleksandras Kostarevas 2024-02-27 12:06:51 +02:00
parent 5888f87fd9
commit f351a61d42
13 changed files with 451 additions and 24 deletions

View File

@ -37,6 +37,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- A signature-protected permission to ask AOSP Keyboard to close the software keyboard. <!-- A signature-protected permission to ask AOSP Keyboard to close the software keyboard.
To use this, add the following line into calling application's AndroidManifest.xml To use this, add the following line into calling application's AndroidManifest.xml
@ -169,6 +170,8 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.futo.inputmethod.updates.InstallReceiver" />
<!-- Content providers --> <!-- Content providers -->
<provider android:name="org.futo.inputmethod.dictionarypack.DictionaryProvider" <provider android:name="org.futo.inputmethod.dictionarypack.DictionaryProvider"
android:grantUriPermissions="true" android:grantUriPermissions="true"

View File

@ -35,4 +35,5 @@
<string name="model_import_failed">Model import failed</string> <string name="model_import_failed">Model import failed</string>
<string name="failed_to_import_the_selected_model">Failed to import the selected model</string> <string name="failed_to_import_the_selected_model">Failed to import the selected model</string>
<string name="dismiss">Dismiss</string> <string name="dismiss">Dismiss</string>
<string name="update">Update</string>
</resources> </resources>

View File

@ -38,6 +38,7 @@ import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.futo.inputmethod.latin.uix.BasicThemeProvider import org.futo.inputmethod.latin.uix.BasicThemeProvider
import org.futo.inputmethod.latin.uix.DynamicThemeProvider import org.futo.inputmethod.latin.uix.DynamicThemeProvider
@ -213,6 +214,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
languageModelFacilitator.loadHistoryLog() languageModelFacilitator.loadHistoryLog()
scheduleUpdateCheckingJob(this) scheduleUpdateCheckingJob(this)
lifecycleScope.launch { uixManager.showUpdateNoticeIfNeeded() }
} }
override fun onDestroy() { override fun onDestroy() {

View File

@ -1,5 +1,6 @@
package org.futo.inputmethod.latin.uix package org.futo.inputmethod.latin.uix
import android.content.Context
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -105,6 +106,12 @@ import kotlin.math.roundToInt
* TODO: Will need to make RTL languages work * TODO: Will need to make RTL languages work
*/ */
interface ImportantNotice {
@Composable fun getText(): String
fun onDismiss(context: Context)
fun onOpen(context: Context)
}
val suggestionStylePrimary = TextStyle( val suggestionStylePrimary = TextStyle(
fontFamily = FontFamily.SansSerif, fontFamily = FontFamily.SansSerif,
@ -386,6 +393,45 @@ fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
} }
} }
@Composable
fun ImportantNoticeView(
importantNotice: ImportantNotice
) {
val context = LocalContext.current
Row {
TextButton(
onClick = { importantNotice.onOpen(context) },
modifier = Modifier
.weight(1.0f)
.fillMaxHeight(),
shape = RectangleShape,
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onBackground),
enabled = true
) {
AutoFitText(importantNotice.getText(), style = suggestionStylePrimary.copy(color = MaterialTheme.colorScheme.onBackground))
}
val color = MaterialTheme.colorScheme.primary
IconButton(
onClick = { importantNotice.onDismiss(context) },
modifier = Modifier
.width(42.dp)
.fillMaxHeight()
.drawBehind {
drawCircle(color = color, radius = size.width / 3.0f + 1.0f)
},
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
) {
Icon(
painter = painterResource(id = R.drawable.close),
contentDescription = "Close"
)
}
}
}
@Composable @Composable
fun ActionBar( fun ActionBar(
words: SuggestedWords?, words: SuggestedWords?,
@ -393,7 +439,9 @@ fun ActionBar(
onActionActivated: (Action) -> Unit, onActionActivated: (Action) -> Unit,
inlineSuggestions: List<MutableState<View?>>, inlineSuggestions: List<MutableState<View?>>,
forceOpenActionsInitially: Boolean = false, forceOpenActionsInitially: Boolean = false,
importantNotice: ImportantNotice? = null
) { ) {
val context = LocalContext.current
val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) } val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) }
Surface(modifier = Modifier Surface(modifier = Modifier
@ -401,28 +449,38 @@ fun ActionBar(
.height(40.dp), color = MaterialTheme.colorScheme.background) .height(40.dp), color = MaterialTheme.colorScheme.background)
{ {
Row { Row {
ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value } ExpandActionsButton(isActionsOpen.value) {
isActionsOpen.value = !isActionsOpen.value
if(isActionsOpen.value) { if(isActionsOpen.value && importantNotice != null) {
LazyRow { importantNotice.onDismiss(context)
item {
ActionItems(onActionActivated)
}
} }
} else if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions)
} else if(words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
words.getInfo(it)
)
}
} else {
Spacer(modifier = Modifier.weight(1.0f))
} }
if(!isActionsOpen.value) { if(importantNotice != null && !isActionsOpen.value) {
ActionItemSmall(VoiceInputAction, onActionActivated) ImportantNoticeView(importantNotice)
}else {
if (isActionsOpen.value) {
LazyRow {
item {
ActionItems(onActionActivated)
}
}
} else if (inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions)
} else if (words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
words.getInfo(it)
)
}
} else {
Spacer(modifier = Modifier.weight(1.0f))
}
if (!isActionsOpen.value) {
ActionItemSmall(VoiceInputAction, onActionActivated)
}
} }
} }
} }
@ -589,6 +647,34 @@ fun PreviewActionBarWithSuggestions(colorScheme: ColorScheme = DarkColorScheme)
} }
} }
@Composable
@Preview
fun PreviewActionBarWithNotice(colorScheme: ColorScheme = DarkColorScheme) {
UixThemeWrapper(colorScheme) {
ActionBar(
words = exampleSuggestedWords,
suggestionStripListener = ExampleListener(),
onActionActivated = { },
inlineSuggestions = listOf(),
importantNotice = object : ImportantNotice {
@Composable
override fun getText(): String {
return "Update available: v1.2.3"
}
override fun onDismiss(context: Context) {
TODO("Not yet implemented")
}
override fun onOpen(context: Context) {
TODO("Not yet implemented")
}
}
)
}
}
@Composable @Composable
@Preview @Preview
fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorScheme) { fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorScheme) {

View File

@ -1,6 +1,8 @@
package org.futo.inputmethod.latin.uix package org.futo.inputmethod.latin.uix
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.inputmethod.InlineSuggestionsResponse import android.view.inputmethod.InlineSuggestionsResponse
@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
@ -27,8 +30,12 @@ import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.inputlogic.InputLogic import org.futo.inputmethod.latin.inputlogic.InputLogic
import org.futo.inputmethod.latin.suggestions.SuggestionStripView import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.actions.EmojiAction import org.futo.inputmethod.latin.uix.actions.EmojiAction
import org.futo.inputmethod.latin.uix.settings.SettingsActivity
import org.futo.inputmethod.latin.uix.theme.ThemeOption import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import org.futo.inputmethod.latin.uix.voiceinput.downloader.DownloadActivity
import org.futo.inputmethod.updates.checkForUpdateAndSaveToPreferences
import org.futo.inputmethod.updates.retrieveSavedLastUpdateCheckResult
private class LatinIMEActionInputTransaction( private class LatinIMEActionInputTransaction(
private val inputLogic: InputLogic, private val inputLogic: InputLogic,
@ -141,6 +148,7 @@ class UixManager(private val latinIME: LatinIME) {
private val keyboardManagerForAction = UixActionKeyboardManager(this, latinIME) private val keyboardManagerForAction = UixActionKeyboardManager(this, latinIME)
private var mainKeyboardHidden = false private var mainKeyboardHidden = false
private var currentNotice: MutableState<ImportantNotice?> = mutableStateOf(null)
var currWindowActionWindow: ActionWindow? = null var currWindowActionWindow: ActionWindow? = null
@ -172,7 +180,8 @@ class UixManager(private val latinIME: LatinIME) {
suggestedWordsOrNull, suggestedWordsOrNull,
latinIME.latinIMELegacy as SuggestionStripView.Listener, latinIME.latinIMELegacy as SuggestionStripView.Listener,
inlineSuggestions = inlineSuggestions, inlineSuggestions = inlineSuggestions,
onActionActivated = { onActionActivated(it) } onActionActivated = { onActionActivated(it) },
importantNotice = currentNotice.value
) )
} }
} }
@ -286,6 +295,35 @@ class UixManager(private val latinIME: LatinIME) {
} }
} }
suspend fun showUpdateNoticeIfNeeded() {
val updateInfo = retrieveSavedLastUpdateCheckResult(latinIME)
if(updateInfo != null && updateInfo.isNewer()) {
currentNotice.value = object : ImportantNotice {
@Composable
override fun getText(): String {
return "Update available: ${updateInfo.nextVersionString}"
}
override fun onDismiss(context: Context) {
currentNotice.value = null
}
override fun onOpen(context: Context) {
currentNotice.value = null
val intent = Intent(context, SettingsActivity::class.java)
intent.putExtra("navDest", "update")
if(context !is Activity) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
}
}
}
fun createComposeView(): View { fun createComposeView(): View {
if(composeView != null) { if(composeView != null) {
composeView = null composeView = null

View File

@ -167,6 +167,17 @@ class SettingsActivity : ComponentActivity() {
this.themeOption.value = themeOption this.themeOption.value = themeOption
} }
val intent = intent
if(intent != null) {
val destination = intent.getStringExtra("navDest")
if(destination != null) {
lifecycleScope.launch {
delay(1000L)
navController.navigate(destination)
}
}
}
} }
override fun onResume() { override fun onResume() {

View File

@ -57,6 +57,9 @@ fun SettingsNavigator(
navController navController
) )
} }
dialog("update") {
UpdateDialog(navController = navController)
}
addModelManagerNavigation(navController) addModelManagerNavigation(navController)
} }
} }

View File

@ -0,0 +1,215 @@
package org.futo.inputmethod.latin.uix.settings
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.InfoDialog
import org.futo.inputmethod.latin.uix.getSetting
import org.futo.inputmethod.updates.InstallReceiver
import org.futo.inputmethod.updates.LAST_UPDATE_CHECK_RESULT
import org.futo.inputmethod.updates.UpdateResult
import java.io.InputStream
import java.io.OutputStream
private fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: OutputStream, onProgress: (Float) -> Unit) {
val buffer = ByteArray(16384);
var n: Int;
var total = 0;
val inputStreamLengthFloat = inputStreamLength.toFloat();
while (read(buffer).also { n = it } >= 0) {
total += n;
outputStream.write(buffer, 0, n);
onProgress.invoke(total.toFloat() / inputStreamLengthFloat);
}
}
private suspend fun install(scope: CoroutineScope, context: Context, inputStream: InputStream, dataLength: Long, updateStatusText: (String) -> Unit) {
var lastProgressText = "";
var session: PackageInstaller.Session? = null;
try {
//Log.i("UpdateScreen", "Hooked InstallReceiver.onReceiveResult.")
InstallReceiver.onReceiveResult.onEach { message -> updateStatusText("Fatal error: $message") }.launchIn(scope)
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller;
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
val sessionId = packageInstaller.createSession(params);
session = packageInstaller.openSession(sessionId)
session.openWrite("package", 0, dataLength).use { sessionStream ->
inputStream.copyToOutputStream(dataLength, sessionStream) { progress ->
val progressText = "${(progress * 100.0f).toInt()}%";
if (lastProgressText != progressText) {
lastProgressText = progressText;
//TODO: Use proper scope
//GlobalScope.launch(Dispatchers.Main) {
//_textProgress.text = progressText;
//};
}
}
session.fsync(sessionStream);
};
val intent = Intent(context, InstallReceiver::class.java);
val pendingIntent = getBroadcast(context, 0, intent, FLAG_MUTABLE or FLAG_UPDATE_CURRENT);
val statusReceiver = pendingIntent.intentSender;
session.commit(statusReceiver);
session.close();
withContext(Dispatchers.Main) {
updateStatusText("Installing update")
//_textProgress.text = "";
//_text.text = context.resources.getText(R.string.installing_update);
}
} catch (e: Throwable) {
Log.w("UpdateScreen", "Exception thrown while downloading and installing latest version of app.", e);
session?.abandon();
withContext(Dispatchers.Main) {
updateStatusText("Failed to install update")
}
} finally {
withContext(Dispatchers.Main) {
Log.i("UpdateScreen", "Keep screen on unset install")
//window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
}
@DelicateCoroutinesApi
private suspend fun downloadAndInstall(scope: CoroutineScope, context: Context, updateResult: UpdateResult, updateStatusText: (String) -> Unit) = GlobalScope.launch(Dispatchers.IO) {
var inputStream: InputStream? = null;
try {
val httpClient = OkHttpClient()
val request = Request.Builder().method("GET", null).url(updateResult.apkUrl).build()
val response = httpClient.newCall(request).execute()
val body = response.body
if (response.isSuccessful && body != null) {
inputStream = body.byteStream();
val dataLength = body.contentLength();
install(scope, context, inputStream, dataLength, updateStatusText)
} else {
throw Exception("Failed to download latest version of app.");
}
} catch (e: Throwable) {
Log.w("UpdateScreen", "Exception thrown while downloading and installing latest version of app.", e);
withContext(Dispatchers.Main) {
updateStatusText("Failed to download update: ${e.message}");
}
} finally {
inputStream?.close();
}
}
@Composable
fun UpdateDialog(navController: NavHostController) {
val scope = LocalLifecycleOwner.current
val context = LocalContext.current
val updateInfo = remember { runBlocking {
context.getSetting(LAST_UPDATE_CHECK_RESULT, "")
} }
val lastUpdateResult = if(!LocalInspectionMode.current){
remember { UpdateResult.fromString(updateInfo) }
} else {
UpdateResult(123, "abc", "1.2.3")
}
val isDownloading = remember { mutableStateOf(false) }
val showSpinner = remember { mutableStateOf(true) }
val statusText = remember { mutableStateOf("Downloading ${lastUpdateResult?.nextVersionString}") }
if(lastUpdateResult == null || !lastUpdateResult.isNewer()) {
InfoDialog(title = "Up-to-date", body = "As of the last update check, the app is up to date.")
} else {
AlertDialog(
icon = {
Icon(Icons.Filled.Info, contentDescription = "Info")
},
title = {
Text(text = "Update available")
},
text = {
if(isDownloading.value) {
Column(modifier = Modifier.fillMaxWidth()) {
if(showSpinner.value) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onSurface
)
}
Text(text = statusText.value)
}
} else {
Text(text = "A new version ${lastUpdateResult.nextVersionString} is available, would you like to update?")
}
},
onDismissRequest = {
if(!isDownloading.value) {
navController.navigateUp()
}
},
confirmButton = {
TextButton(onClick = {
isDownloading.value = true
GlobalScope.launch { downloadAndInstall(scope.lifecycleScope, context, lastUpdateResult) {
statusText.value = it
showSpinner.value = false
} }
}, enabled = !isDownloading.value) {
Text(stringResource(R.string.update))
}
},
dismissButton = {
TextButton(onClick = {
navController.navigateUp()
}, enabled = !isDownloading.value) {
Text(stringResource(R.string.dismiss))
}
}
)
}
}

View File

@ -31,7 +31,7 @@ fun HomeScreen(navController: NavHostController = rememberNavController()) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
ScreenTitle("FUTO Keyboard Settings") ScreenTitle("FUTO Keyboard Settings")
ConditionalUpdate() ConditionalUpdate(navController)
NavigationItem( NavigationItem(
title = "Languages", title = "Languages",

View File

@ -0,0 +1,57 @@
package org.futo.inputmethod.updates
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.util.Log
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
class InstallReceiver : BroadcastReceiver() {
private val TAG = "InstallReceiver"
companion object {
val onReceiveResult = MutableSharedFlow<String>(0)
}
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
Log.i(TAG, "Received status $status.")
GlobalScope.launch {
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val activityIntent: Intent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}
if (activityIntent == null) {
Log.w(TAG, "Received STATUS_PENDING_USER_ACTION and activity intent is null.")
return@launch
}
context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
PackageInstaller.STATUS_SUCCESS -> onReceiveResult.emit("Success!")
PackageInstaller.STATUS_FAILURE -> onReceiveResult.emit("General failure")
PackageInstaller.STATUS_FAILURE_ABORTED -> onReceiveResult.emit("The operation failed because it was actively aborted")
PackageInstaller.STATUS_FAILURE_BLOCKED -> onReceiveResult.emit("The operation failed because it was blocked")
PackageInstaller.STATUS_FAILURE_CONFLICT -> onReceiveResult.emit("The operation failed because it conflicts (or is inconsistent with) with another package already installed on the device")
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> onReceiveResult.emit("The operation failed because it is fundamentally incompatible with this device")
PackageInstaller.STATUS_FAILURE_INVALID -> onReceiveResult.emit("The operation failed because one or more of the APKs was invalid")
PackageInstaller.STATUS_FAILURE_STORAGE -> onReceiveResult.emit("The operation failed because of storage issues")
else -> {
val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
if(msg != null) {
onReceiveResult.emit(msg)
}
}
}
}
}
}

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.navigation.NavHostController
import org.futo.inputmethod.latin.uix.settings.SettingItem import org.futo.inputmethod.latin.uix.settings.SettingItem
import org.futo.inputmethod.latin.uix.settings.useDataStore import org.futo.inputmethod.latin.uix.settings.useDataStore
@ -33,7 +34,7 @@ fun Context.openURI(uri: String, newTask: Boolean = false) {
@Composable @Composable
@Preview @Preview
fun ConditionalUpdate() { fun ConditionalUpdate(navController: NavHostController) {
val (updateInfo, _) = useDataStore(key = LAST_UPDATE_CHECK_RESULT, default = "") val (updateInfo, _) = useDataStore(key = LAST_UPDATE_CHECK_RESULT, default = "")
val lastUpdateResult = if(!LocalInspectionMode.current){ val lastUpdateResult = if(!LocalInspectionMode.current){
@ -48,7 +49,8 @@ fun ConditionalUpdate() {
title = "Update Available", title = "Update Available",
subtitle = "${UpdateResult.currentVersionString()} -> ${lastUpdateResult.nextVersionString}", subtitle = "${UpdateResult.currentVersionString()} -> ${lastUpdateResult.nextVersionString}",
onClick = { onClick = {
context.openURI(lastUpdateResult.apkUrl) navController.navigate("update")
//context.openURI(lastUpdateResult.apkUrl)
} }
) { ) {
Icon(Icons.Default.ArrowForward, contentDescription = "Go") Icon(Icons.Default.ArrowForward, contentDescription = "Go")

View File

@ -30,7 +30,6 @@ suspend fun checkForUpdate(): UpdateResult? {
val response = httpClient.newCall(request).execute() val response = httpClient.newCall(request).execute()
val body = response.body val body = response.body
val result = if (body != null) { val result = if (body != null) {
val data = body.string().lines() val data = body.string().lines()
body.closeQuietly() body.closeQuietly()
@ -39,15 +38,18 @@ suspend fun checkForUpdate(): UpdateResult? {
val latestVersionUrl = data[1] val latestVersionUrl = data[1]
val latestVersionString = data[2] val latestVersionString = data[2]
if(latestVersionUrl.startsWith("https://voiceinput.futo.org/") || latestVersionUrl.startsWith("https://keyboard.futo.org/")){ if(latestVersionUrl.startsWith("https://voiceinput.futo.org/") || latestVersionUrl.startsWith("https://keyboard.futo.org/")){
Log.d("UpdateChecking", "Retrieved update for version ${latestVersionString}")
UpdateResult( UpdateResult(
nextVersion = latestVersion, nextVersion = latestVersion,
apkUrl = latestVersionUrl, apkUrl = latestVersionUrl,
nextVersionString = latestVersionString nextVersionString = latestVersionString
) )
} else { } else {
Log.e("UpdateChecking", "Update URL contains unknown prefix: ${latestVersionUrl}")
null null
} }
} else { } else {
Log.e("UpdateChecking", "Body of result is null")
null null
} }
@ -55,6 +57,8 @@ suspend fun checkForUpdate(): UpdateResult? {
result result
} catch (e: Exception) { } catch (e: Exception) {
Log.e("UpdateChecking", "Checking update failed with exception")
e.printStackTrace()
null null
} }
} }

View File

@ -1,5 +1,6 @@
package org.futo.inputmethod.updates package org.futo.inputmethod.updates
import android.util.Log
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -32,8 +33,12 @@ data class UpdateResult(
try { try {
return Json.decodeFromString<UpdateResult>(value) return Json.decodeFromString<UpdateResult>(value)
} catch(e: SerializationException) { } catch(e: SerializationException) {
Log.e("UpdateResult", "Failed to deserialize UpdateResult value $value")
e.printStackTrace()
return null return null
} catch(e: IllegalArgumentException) { } catch(e: IllegalArgumentException) {
Log.e("UpdateResult", "Failed to deserialize UpdateResult value $value")
e.printStackTrace()
return null return null
} }
} }