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.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"
|
||||||
|
@ -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>
|
@ -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() {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -57,6 +57,9 @@ fun SettingsNavigator(
|
|||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
dialog("update") {
|
||||||
|
UpdateDialog(navController = navController)
|
||||||
|
}
|
||||||
addModelManagerNavigation(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))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
ScreenTitle("FUTO Keyboard Settings")
|
ScreenTitle("FUTO Keyboard Settings")
|
||||||
|
|
||||||
ConditionalUpdate()
|
ConditionalUpdate(navController)
|
||||||
|
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
title = "Languages",
|
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.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")
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user