mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Add update important notice, download and install update from within the app
This commit is contained in:
parent
5888f87fd9
commit
f351a61d42
@ -37,6 +37,7 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<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.
|
||||
To use this, add the following line into calling application's AndroidManifest.xml
|
||||
@ -169,6 +170,8 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.futo.inputmethod.updates.InstallReceiver" />
|
||||
|
||||
<!-- Content providers -->
|
||||
<provider android:name="org.futo.inputmethod.dictionarypack.DictionaryProvider"
|
||||
android:grantUriPermissions="true"
|
||||
|
@ -35,4 +35,5 @@
|
||||
<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="dismiss">Dismiss</string>
|
||||
<string name="update">Update</string>
|
||||
</resources>
|
@ -38,6 +38,7 @@ import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.futo.inputmethod.latin.uix.BasicThemeProvider
|
||||
import org.futo.inputmethod.latin.uix.DynamicThemeProvider
|
||||
@ -213,6 +214,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
languageModelFacilitator.loadHistoryLog()
|
||||
|
||||
scheduleUpdateCheckingJob(this)
|
||||
lifecycleScope.launch { uixManager.showUpdateNoticeIfNeeded() }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.futo.inputmethod.latin.uix
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
@ -105,6 +106,12 @@ import kotlin.math.roundToInt
|
||||
* 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(
|
||||
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
|
||||
fun ActionBar(
|
||||
words: SuggestedWords?,
|
||||
@ -393,7 +439,9 @@ fun ActionBar(
|
||||
onActionActivated: (Action) -> Unit,
|
||||
inlineSuggestions: List<MutableState<View?>>,
|
||||
forceOpenActionsInitially: Boolean = false,
|
||||
importantNotice: ImportantNotice? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) }
|
||||
|
||||
Surface(modifier = Modifier
|
||||
@ -401,28 +449,38 @@ fun ActionBar(
|
||||
.height(40.dp), color = MaterialTheme.colorScheme.background)
|
||||
{
|
||||
Row {
|
||||
ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value }
|
||||
|
||||
if(isActionsOpen.value) {
|
||||
LazyRow {
|
||||
item {
|
||||
ActionItems(onActionActivated)
|
||||
}
|
||||
ExpandActionsButton(isActionsOpen.value) {
|
||||
isActionsOpen.value = !isActionsOpen.value
|
||||
if(isActionsOpen.value && importantNotice != null) {
|
||||
importantNotice.onDismiss(context)
|
||||
}
|
||||
} 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)
|
||||
if(importantNotice != null && !isActionsOpen.value) {
|
||||
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
|
||||
@Preview
|
||||
fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorScheme) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package org.futo.inputmethod.latin.uix
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InlineSuggestionsResponse
|
||||
@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
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.suggestions.SuggestionStripView
|
||||
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.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 val inputLogic: InputLogic,
|
||||
@ -141,6 +148,7 @@ class UixManager(private val latinIME: LatinIME) {
|
||||
private val keyboardManagerForAction = UixActionKeyboardManager(this, latinIME)
|
||||
|
||||
private var mainKeyboardHidden = false
|
||||
private var currentNotice: MutableState<ImportantNotice?> = mutableStateOf(null)
|
||||
|
||||
var currWindowActionWindow: ActionWindow? = null
|
||||
|
||||
@ -172,7 +180,8 @@ class UixManager(private val latinIME: LatinIME) {
|
||||
suggestedWordsOrNull,
|
||||
latinIME.latinIMELegacy as SuggestionStripView.Listener,
|
||||
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 {
|
||||
if(composeView != null) {
|
||||
composeView = null
|
||||
|
@ -167,6 +167,17 @@ class SettingsActivity : ComponentActivity() {
|
||||
|
||||
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() {
|
||||
|
@ -57,6 +57,9 @@ fun SettingsNavigator(
|
||||
navController
|
||||
)
|
||||
}
|
||||
dialog("update") {
|
||||
UpdateDialog(navController = navController)
|
||||
}
|
||||
addModelManagerNavigation(navController)
|
||||
}
|
||||
}
|
215
java/src/org/futo/inputmethod/latin/uix/settings/UpdateScreen.kt
Normal file
215
java/src/org/futo/inputmethod/latin/uix/settings/UpdateScreen.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ fun HomeScreen(navController: NavHostController = rememberNavController()) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
ScreenTitle("FUTO Keyboard Settings")
|
||||
|
||||
ConditionalUpdate()
|
||||
ConditionalUpdate(navController)
|
||||
|
||||
NavigationItem(
|
||||
title = "Languages",
|
||||
|
57
java/src/org/futo/inputmethod/updates/InstallReceiver.kt
Normal file
57
java/src/org/futo/inputmethod/updates/InstallReceiver.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.useDataStore
|
||||
|
||||
@ -33,7 +34,7 @@ fun Context.openURI(uri: String, newTask: Boolean = false) {
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ConditionalUpdate() {
|
||||
fun ConditionalUpdate(navController: NavHostController) {
|
||||
val (updateInfo, _) = useDataStore(key = LAST_UPDATE_CHECK_RESULT, default = "")
|
||||
|
||||
val lastUpdateResult = if(!LocalInspectionMode.current){
|
||||
@ -48,7 +49,8 @@ fun ConditionalUpdate() {
|
||||
title = "Update Available",
|
||||
subtitle = "${UpdateResult.currentVersionString()} -> ${lastUpdateResult.nextVersionString}",
|
||||
onClick = {
|
||||
context.openURI(lastUpdateResult.apkUrl)
|
||||
navController.navigate("update")
|
||||
//context.openURI(lastUpdateResult.apkUrl)
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.ArrowForward, contentDescription = "Go")
|
||||
|
@ -30,7 +30,6 @@ suspend fun checkForUpdate(): UpdateResult? {
|
||||
val response = httpClient.newCall(request).execute()
|
||||
|
||||
val body = response.body
|
||||
|
||||
val result = if (body != null) {
|
||||
val data = body.string().lines()
|
||||
body.closeQuietly()
|
||||
@ -39,15 +38,18 @@ suspend fun checkForUpdate(): UpdateResult? {
|
||||
val latestVersionUrl = data[1]
|
||||
val latestVersionString = data[2]
|
||||
if(latestVersionUrl.startsWith("https://voiceinput.futo.org/") || latestVersionUrl.startsWith("https://keyboard.futo.org/")){
|
||||
Log.d("UpdateChecking", "Retrieved update for version ${latestVersionString}")
|
||||
UpdateResult(
|
||||
nextVersion = latestVersion,
|
||||
apkUrl = latestVersionUrl,
|
||||
nextVersionString = latestVersionString
|
||||
)
|
||||
} else {
|
||||
Log.e("UpdateChecking", "Update URL contains unknown prefix: ${latestVersionUrl}")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.e("UpdateChecking", "Body of result is null")
|
||||
null
|
||||
}
|
||||
|
||||
@ -55,6 +57,8 @@ suspend fun checkForUpdate(): UpdateResult? {
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("UpdateChecking", "Checking update failed with exception")
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.futo.inputmethod.updates
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -32,8 +33,12 @@ data class UpdateResult(
|
||||
try {
|
||||
return Json.decodeFromString<UpdateResult>(value)
|
||||
} catch(e: SerializationException) {
|
||||
Log.e("UpdateResult", "Failed to deserialize UpdateResult value $value")
|
||||
e.printStackTrace()
|
||||
return null
|
||||
} catch(e: IllegalArgumentException) {
|
||||
Log.e("UpdateResult", "Failed to deserialize UpdateResult value $value")
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user