mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Add automatic update checking service
This commit is contained in:
parent
fda0052c55
commit
74c74c7ba7
@ -22,7 +22,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdk 24
|
||||
targetSdk 34
|
||||
versionName "1.0"
|
||||
versionName "0.1"
|
||||
versionCode 31
|
||||
|
||||
applicationId 'org.futo.inputmethod.latin'
|
||||
testApplicationId 'org.futo.inputmethod.latin.tests'
|
||||
|
@ -15,12 +15,12 @@
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.futo.inputmethod.latin"
|
||||
android:versionCode="30">
|
||||
package="org.futo.inputmethod.latin">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||
@ -74,6 +74,12 @@
|
||||
android:resource="@xml/method"/>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="org.futo.inputmethod.updates.UpdateCheckingService"
|
||||
android:label="@string/update_checking_service"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" >
|
||||
</service>
|
||||
|
||||
<service android:name=".spellcheck.AndroidSpellCheckerService"
|
||||
android:label="@string/spell_checker_service_name"
|
||||
android:permission="android.permission.BIND_TEXT_SERVICE"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="voice_input_action_title">Voice Input</string>
|
||||
<string name="theme_switcher_action_title">Theme Switcher</string>
|
||||
<string name="emoji_action_title">Emojis</string>
|
||||
@ -26,4 +26,8 @@
|
||||
<string name="download_in_progress">Downloading models…</string>
|
||||
|
||||
<string name="model_downloader">Model Downloader</string>
|
||||
|
||||
<string name="update_checking_service">Update Checking service</string>
|
||||
<string name="update_available">Update Available</string>
|
||||
<string name="update_available_notification">An update is available (<xliff:g name="versionDiff" example="v1 -> v2">%s</xliff:g>). Tap to download</string>
|
||||
</resources>
|
@ -53,6 +53,7 @@ import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
|
||||
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
|
||||
import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator
|
||||
import org.futo.inputmethod.updates.scheduleUpdateCheckingJob
|
||||
|
||||
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner,
|
||||
LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner {
|
||||
@ -210,6 +211,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
|
||||
|
||||
languageModelFacilitator.launchProcessor()
|
||||
languageModelFacilitator.loadHistoryLog()
|
||||
|
||||
scheduleUpdateCheckingJob(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -1,29 +1,38 @@
|
||||
package org.futo.inputmethod.latin.uix.settings.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.futo.inputmethod.latin.BuildConfig
|
||||
import org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItem
|
||||
import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScreenTitle
|
||||
import org.futo.inputmethod.latin.uix.settings.ScrollableList
|
||||
import org.futo.inputmethod.latin.uix.settings.openLanguageSettings
|
||||
import org.futo.inputmethod.latin.uix.theme.Typography
|
||||
import org.futo.inputmethod.updates.ConditionalUpdate
|
||||
|
||||
@Preview
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun HomeScreen(navController: NavHostController = rememberNavController()) {
|
||||
val context = LocalContext.current
|
||||
ScrollableList {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
ScreenTitle("FUTO Keyboard Settings")
|
||||
|
||||
ConditionalUpdate()
|
||||
|
||||
NavigationItem(
|
||||
title = "Languages",
|
||||
style = NavigationItemStyle.HomePrimary,
|
||||
@ -59,13 +68,14 @@ fun HomeScreen(navController: NavHostController = rememberNavController()) {
|
||||
icon = painterResource(id = R.drawable.eye)
|
||||
)
|
||||
|
||||
/*
|
||||
NavigationItem(
|
||||
title = "Advanced",
|
||||
style = NavigationItemStyle.Misc,
|
||||
navigate = { },
|
||||
icon = painterResource(id = R.drawable.delete)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Text(
|
||||
"v${BuildConfig.VERSION_NAME}",
|
||||
style = Typography.labelSmall,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
*/
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
58
java/src/org/futo/inputmethod/updates/Update.kt
Normal file
58
java/src/org/futo/inputmethod/updates/Update.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package org.futo.inputmethod.updates
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
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 org.futo.inputmethod.latin.uix.settings.SettingItem
|
||||
import org.futo.inputmethod.latin.uix.settings.useDataStore
|
||||
|
||||
val LAST_UPDATE_CHECK_RESULT = stringPreferencesKey("last_update_check_result")
|
||||
|
||||
fun Context.openURI(uri: String, newTask: Boolean = false) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
|
||||
if (newTask) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
} catch(e: ActivityNotFoundException) {
|
||||
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ConditionalUpdate() {
|
||||
val (updateInfo, _) = useDataStore(key = LAST_UPDATE_CHECK_RESULT, default = "")
|
||||
|
||||
val lastUpdateResult = if(!LocalInspectionMode.current){
|
||||
UpdateResult.fromString(updateInfo)
|
||||
} else {
|
||||
UpdateResult(123, "abc", "1.2.3")
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
if(lastUpdateResult != null && lastUpdateResult.isNewer()) {
|
||||
SettingItem(
|
||||
title = "Update Available",
|
||||
subtitle = "${UpdateResult.currentVersionString()} -> ${lastUpdateResult.nextVersionString}",
|
||||
onClick = {
|
||||
context.openURI(lastUpdateResult.apkUrl)
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.ArrowForward, contentDescription = "Go")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
101
java/src/org/futo/inputmethod/updates/UpdateChecking.kt
Normal file
101
java/src/org/futo/inputmethod/updates/UpdateChecking.kt
Normal file
@ -0,0 +1,101 @@
|
||||
package org.futo.inputmethod.updates
|
||||
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.futo.inputmethod.latin.uix.dataStore
|
||||
import org.futo.inputmethod.latin.uix.getSetting
|
||||
import java.lang.Exception
|
||||
|
||||
const val UPDATE_URL = "https://voiceinput.futo.org/SuperSecretKeyboard/keyboard_version"
|
||||
|
||||
suspend fun checkForUpdate(): UpdateResult? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val httpClient = OkHttpClient()
|
||||
|
||||
val request = Request.Builder().method("GET", null).url(UPDATE_URL).build()
|
||||
|
||||
try {
|
||||
val response = httpClient.newCall(request).execute()
|
||||
|
||||
val body = response.body
|
||||
|
||||
val result = if (body != null) {
|
||||
val data = body.string().lines()
|
||||
body.closeQuietly()
|
||||
|
||||
val latestVersion = data[0].toInt()
|
||||
val latestVersionUrl = data[1]
|
||||
val latestVersionString = data[2]
|
||||
if(latestVersionUrl.startsWith("https://voiceinput.futo.org/") || latestVersionUrl.startsWith("https://keyboard.futo.org/")){
|
||||
UpdateResult(
|
||||
nextVersion = latestVersion,
|
||||
apkUrl = latestVersionUrl,
|
||||
nextVersionString = latestVersionString
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
response.closeQuietly()
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdateAndSaveToPreferences(context: Context): Boolean {
|
||||
val updateResult = checkForUpdate()
|
||||
if(updateResult != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
context.dataStore.edit {
|
||||
it[LAST_UPDATE_CHECK_RESULT] = Json.encodeToString(updateResult)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun retrieveSavedLastUpdateCheckResult(context: Context): UpdateResult? {
|
||||
return UpdateResult.fromString(context.getSetting(LAST_UPDATE_CHECK_RESULT, ""))
|
||||
}
|
||||
|
||||
const val JOB_ID: Int = 15782788
|
||||
fun scheduleUpdateCheckingJob(context: Context) {
|
||||
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
|
||||
if(jobScheduler.getPendingJob(JOB_ID) != null) {
|
||||
Log.i("UpdateChecking", "Job already scheduled, no need to do anything")
|
||||
return
|
||||
}
|
||||
|
||||
var jobInfoBuilder = JobInfo.Builder(JOB_ID, ComponentName(context, UpdateCheckingService::class.java))
|
||||
.setPeriodic(1000 * 60 * 60 * 24 * 2) // every two days
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // on unmetered Wi-Fi
|
||||
.setPersisted(true) // persist after reboots
|
||||
|
||||
// Update checking has minimum priority
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
jobInfoBuilder = jobInfoBuilder.setPriority(JobInfo.PRIORITY_MIN)
|
||||
}
|
||||
|
||||
jobScheduler.schedule(jobInfoBuilder.build())
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package org.futo.inputmethod.updates
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.futo.inputmethod.latin.R
|
||||
|
||||
const val CHANNEL_ID = "UPDATES"
|
||||
const val NOTIFICATION_ID = 1
|
||||
|
||||
class UpdateCheckingService : JobService() {
|
||||
private var job: Job? = null
|
||||
override fun onStartJob(params: JobParameters?): Boolean {
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
if(checkForUpdateAndSaveToPreferences(applicationContext)) {
|
||||
val updateResult = retrieveSavedLastUpdateCheckResult(applicationContext)
|
||||
|
||||
if(updateResult != null && updateResult.isNewer()) {
|
||||
// Show a notification : "Update available"
|
||||
val manager = applicationContext.getSystemService(
|
||||
Context.NOTIFICATION_SERVICE
|
||||
) as NotificationManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Update Notifications",
|
||||
NotificationManager.IMPORTANCE_MIN
|
||||
)
|
||||
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val contentIntent = PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(updateResult.apkUrl)),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.update_available))
|
||||
.setContentText(getString(R.string.update_available_notification,"${UpdateResult.currentVersionString()} -> ${updateResult.nextVersionString}"))
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentIntent(contentIntent)
|
||||
|
||||
manager.notify(NOTIFICATION_ID, notification.build())
|
||||
}
|
||||
} else {
|
||||
Log.i("UpdateCheckingService", "no update available, or failed to check")
|
||||
}
|
||||
jobFinished(params, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters?): Boolean {
|
||||
job?.cancel()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
41
java/src/org/futo/inputmethod/updates/UpdateResult.kt
Normal file
41
java/src/org/futo/inputmethod/updates/UpdateResult.kt
Normal file
@ -0,0 +1,41 @@
|
||||
package org.futo.inputmethod.updates
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.futo.inputmethod.latin.BuildConfig
|
||||
|
||||
@Serializable
|
||||
data class UpdateResult(
|
||||
val nextVersion: Int,
|
||||
val apkUrl: String,
|
||||
val nextVersionString: String
|
||||
) {
|
||||
fun isNewer(): Boolean {
|
||||
return nextVersion > currentVersion()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun currentVersion(): Int {
|
||||
return BuildConfig.VERSION_CODE
|
||||
}
|
||||
|
||||
fun currentVersionString(): String {
|
||||
return BuildConfig.VERSION_NAME
|
||||
}
|
||||
|
||||
fun fromString(value: String): UpdateResult? {
|
||||
if(value.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return Json.decodeFromString<UpdateResult>(value)
|
||||
} catch(e: SerializationException) {
|
||||
return null
|
||||
} catch(e: IllegalArgumentException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,8 +15,7 @@
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.futo.inputmethod.latin.tests"
|
||||
android:versionCode="30">
|
||||
package="org.futo.inputmethod.latin.tests">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user