Skip to content

Commit e96f493

Browse files
Update apk (#73)
Check update in SettingsPage
2 parents 185adbd + c1ff632 commit e96f493

File tree

15 files changed

+448
-7
lines changed

15 files changed

+448
-7
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
88
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
99
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
10+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
1011
<uses-permission
1112
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
1213
android:maxSdkVersion="32"
@@ -57,9 +58,14 @@
5758
<data android:mimeType="text/plain" />
5859
</intent-filter>
5960
</activity>
61+
6062
<service
6163
android:name=".ui.page.post.DownloadService"
6264
android:foregroundServiceType="dataSync" />
65+
<service
66+
android:name=".worker.DownloadApkService"
67+
android:foregroundServiceType="dataSync" />
68+
6369
<provider
6470
android:name="androidx.core.content.FileProvider"
6571
android:authorities="${applicationId}.fileprovider"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
package com.paulcoding.hviewer
22

33
const val CHECK_FOR_UPDATE_CHANNEL = "check_for_update"
4+
const val CHECK_FOR_UPDATE_APK_CHANNEL = "check_for_update_app"
5+
const val ACTION_INSTALL_APK = "ACTION_INSTALL_APK"

app/src/main/java/com/paulcoding/hviewer/MainApp.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.app.NotificationManager
66
import android.content.Context
77
import com.paulcoding.hviewer.helper.CrashHandler
88
import com.paulcoding.hviewer.helper.setupPaths
9+
import com.paulcoding.hviewer.worker.scheduleApkUpdate
910
import com.paulcoding.hviewer.worker.scheduleScriptsUpdate
1011
import com.paulcoding.js.JS
1112
import com.tencent.mmkv.MMKV
@@ -25,6 +26,7 @@ class MainApp : Application() {
2526

2627
private fun setupWorkers() {
2728
scheduleScriptsUpdate(this)
29+
scheduleApkUpdate(this)
2830
}
2931

3032
private fun setupNotificationChannels() {
@@ -34,7 +36,13 @@ class MainApp : Application() {
3436
"Check for update",
3537
NotificationManager.IMPORTANCE_LOW
3638
)
39+
val checkForUpdateApkChannel = NotificationChannel(
40+
CHECK_FOR_UPDATE_APK_CHANNEL,
41+
"Check for update",
42+
NotificationManager.IMPORTANCE_LOW
43+
)
3744
notificationManager.createNotificationChannel(checkForUpdateChannel)
45+
notificationManager.createNotificationChannel(checkForUpdateApkChannel)
3846
}
3947

4048
companion object {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.paulcoding.hviewer.model
2+
3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
5+
6+
data class Release(
7+
val url: String,
8+
val id: Int,
9+
val tag_name: String,
10+
val assets: List<Asset>,
11+
12+
)
13+
14+
data class Asset(
15+
val browser_download_url: String,
16+
)
17+
18+
@Parcelize
19+
data class HRelease(
20+
val version: String,
21+
val downloadUrl: String
22+
) : Parcelable

app/src/main/java/com/paulcoding/hviewer/network/Github.kt

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.paulcoding.hviewer.network
22

3+
import android.content.Context
34
import com.google.gson.Gson
5+
import com.paulcoding.hviewer.BuildConfig
46
import com.paulcoding.hviewer.MainApp.Companion.appContext
57
import com.paulcoding.hviewer.R
68
import com.paulcoding.hviewer.helper.extractTarGzFromResponseBody
79
import com.paulcoding.hviewer.helper.log
810
import com.paulcoding.hviewer.helper.readConfigFile
11+
import com.paulcoding.hviewer.model.HRelease
12+
import com.paulcoding.hviewer.model.Release
913
import com.paulcoding.hviewer.model.SiteConfigs
1014
import com.paulcoding.hviewer.preference.Preferences
1115
import io.ktor.client.call.body
@@ -14,6 +18,7 @@ import io.ktor.client.statement.readRawBytes
1418
import io.ktor.http.HttpStatusCode
1519
import kotlinx.coroutines.Dispatchers
1620
import kotlinx.coroutines.withContext
21+
import java.io.File
1722

1823
object Github {
1924
@Throws(Exception::class)
@@ -111,6 +116,39 @@ object Github {
111116
return null
112117
}
113118
}
119+
120+
suspend fun checkForUpdate(
121+
currentVersion: String,
122+
onUpdateAvailable: ((String, String) -> Unit)? = null
123+
): HRelease? {
124+
val (owner, repo) = parseRepo(BuildConfig.REPO_URL)
125+
val url = "https://api.github.com/repos/${owner}/${repo}/releases/latest"
126+
ktorClient.use { client ->
127+
val jsonObject: Release = client.get(url).body()
128+
val latestVersion = jsonObject.tag_name.substring(1)
129+
val downloadUrl = jsonObject.assets[0].browser_download_url
130+
if (latestVersion != currentVersion) {
131+
onUpdateAvailable?.invoke(latestVersion, downloadUrl)
132+
return HRelease(latestVersion, downloadUrl)
133+
}
134+
return null
135+
}
136+
}
137+
138+
suspend fun downloadApk(
139+
context: Context,
140+
downloadUrl: String,
141+
onDownloadComplete: (File) -> Unit
142+
) {
143+
val file = File(context.cacheDir, "latest.apk")
144+
ktorClient.use { client ->
145+
val input = client.get(downloadUrl).readRawBytes().inputStream()
146+
file.outputStream().use { output ->
147+
input.copyTo(output)
148+
}
149+
}
150+
onDownloadComplete(file)
151+
}
114152
}
115153

116154

@@ -121,7 +159,7 @@ sealed class SiteConfigsState {
121159

122160
fun getToastMessage() = when (this) {
123161
is NewConfigsInstall -> R.string.scripts_installed
124-
is UpToDate -> R.string.up_to_Date
162+
is UpToDate -> R.string.up_to_date
125163
is Updated -> R.string.scripts_updated
126164
}
127165
}

app/src/main/java/com/paulcoding/hviewer/ui/component/ConfirmDialog.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ fun ConfirmDialog(
1717
text: String = "",
1818
confirmColor: Color? = null,
1919
dismissColor: Color? = null,
20+
confirmText: String? = null,
21+
dismissText: String? = null,
2022
onDismiss: () -> Unit,
2123
onConfirm: () -> Unit
2224
) {
@@ -28,15 +30,15 @@ fun ConfirmDialog(
2830
confirmButton = {
2931
TextButton(onClick = { onConfirm() }) {
3032
Text(
31-
stringResource(R.string.confirm),
33+
confirmText ?: stringResource(R.string.confirm),
3234
color = confirmColor ?: MaterialTheme.colorScheme.error
3335
)
3436
}
3537
},
3638
dismissButton = {
3739
TextButton(onClick = { onDismiss() }) {
3840
Text(
39-
stringResource(R.string.cancel),
41+
dismissText ?: stringResource(R.string.cancel),
4042
color = dismissColor ?: MaterialTheme.colorScheme.onBackground
4143
)
4244
}

app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.runtime.collectAsState
1414
import androidx.compose.runtime.getValue
1515
import androidx.compose.runtime.rememberUpdatedState
1616
import androidx.compose.ui.platform.LocalContext
17+
import androidx.core.net.toUri
1718
import androidx.navigation.NamedNavArgument
1819
import androidx.navigation.NavBackStackEntry
1920
import androidx.navigation.NavDeepLink
@@ -24,6 +25,7 @@ import androidx.navigation.compose.composable
2425
import androidx.navigation.compose.rememberNavController
2526
import androidx.navigation.navArgument
2627
import androidx.navigation.navDeepLink
28+
import com.paulcoding.hviewer.ACTION_INSTALL_APK
2729
import com.paulcoding.hviewer.BuildConfig
2830
import com.paulcoding.hviewer.R
2931
import com.paulcoding.hviewer.helper.makeToast
@@ -100,6 +102,10 @@ fun AppEntry(intent: Intent?, appViewModel: AppViewModel) {
100102
}
101103
}
102104

105+
ACTION_INSTALL_APK -> {
106+
appViewModel.installApk(context, data.toString().toUri())
107+
}
108+
103109
else -> {
104110
}
105111
}

app/src/main/java/com/paulcoding/hviewer/ui/page/AppViewModel.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.paulcoding.hviewer.ui.page
22

3+
import android.content.Context
4+
import android.content.Intent
5+
import android.net.Uri
6+
import androidx.core.content.FileProvider
37
import androidx.lifecycle.ViewModel
48
import androidx.lifecycle.viewModelScope
59
import com.paulcoding.hviewer.BuildConfig
@@ -66,6 +70,7 @@ class AppViewModel : ViewModel() {
6670
val siteConfigs: SiteConfigs? = appContext.readConfigFile<SiteConfigs>().getOrNull(),
6771
val error: Throwable? = null,
6872
val checkingForUpdateScripts: Boolean = false,
73+
val updatingApk: Boolean = false,
6974
)
7075

7176
private fun setError(throwable: Throwable) {
@@ -185,4 +190,43 @@ class AppViewModel : ViewModel() {
185190
}
186191
}
187192
}
193+
194+
fun checkForUpdate(
195+
currentVersion: String,
196+
onUpToDate: () -> Unit,
197+
onUpdateAvailable: (String, String) -> Unit
198+
) {
199+
viewModelScope.launch {
200+
_stateFlow.update { it.copy(updatingApk = true) }
201+
val release = Github.checkForUpdate(currentVersion)
202+
if (release != null)
203+
onUpdateAvailable(release.version, release.downloadUrl)
204+
else
205+
onUpToDate()
206+
_stateFlow.update { it.copy(updatingApk = false) }
207+
}
208+
}
209+
210+
fun downloadAndInstallApk(context: Context, downloadUrl: String) {
211+
viewModelScope.launch {
212+
_stateFlow.update { it.copy(updatingApk = true) }
213+
Github.downloadApk(context, downloadUrl) { file ->
214+
val uri = FileProvider.getUriForFile(
215+
context,
216+
"${context.packageName}.fileprovider",
217+
file
218+
)
219+
installApk(context, uri)
220+
}
221+
_stateFlow.update { it.copy(updatingApk = false) }
222+
}
223+
}
224+
225+
fun installApk(context: Context, uri: Uri) {
226+
val intent = Intent(Intent.ACTION_VIEW).apply {
227+
setDataAndType(uri, "application/vnd.android.package-archive")
228+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
229+
}
230+
context.startActivity(intent)
231+
}
188232
}

app/src/main/java/com/paulcoding/hviewer/ui/page/settings/SettingsPage.kt

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.paulcoding.hviewer.ui.page.settings
22

33
import androidx.activity.ComponentActivity
4+
import androidx.compose.foundation.background
45
import androidx.compose.foundation.clickable
56
import androidx.compose.foundation.interaction.MutableInteractionSource
67
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Box
79
import androidx.compose.foundation.layout.Column
810
import androidx.compose.foundation.layout.Row
911
import androidx.compose.foundation.layout.RowScope
@@ -16,10 +18,12 @@ import androidx.compose.material.icons.Icons
1618
import androidx.compose.material.icons.outlined.BugReport
1719
import androidx.compose.material.icons.outlined.Description
1820
import androidx.compose.material.icons.outlined.Edit
21+
import androidx.compose.material.icons.outlined.Update
1922
import androidx.compose.material3.ExperimentalMaterial3Api
2023
import androidx.compose.material3.HorizontalDivider
2124
import androidx.compose.material3.Icon
2225
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
26+
import androidx.compose.material3.MaterialTheme
2327
import androidx.compose.material3.Scaffold
2428
import androidx.compose.material3.Switch
2529
import androidx.compose.material3.SwitchColors
@@ -35,17 +39,22 @@ import androidx.compose.runtime.remember
3539
import androidx.compose.runtime.setValue
3640
import androidx.compose.ui.Alignment
3741
import androidx.compose.ui.Modifier
42+
import androidx.compose.ui.graphics.Color
3843
import androidx.compose.ui.platform.LocalContext
3944
import androidx.compose.ui.res.stringResource
4045
import androidx.compose.ui.text.font.FontWeight
4146
import androidx.compose.ui.unit.dp
4247
import androidx.compose.ui.unit.sp
48+
import com.paulcoding.hviewer.BuildConfig
4349
import com.paulcoding.hviewer.R
4450
import com.paulcoding.hviewer.extensions.setSecureScreen
4551
import com.paulcoding.hviewer.helper.makeToast
4652
import com.paulcoding.hviewer.preference.Preferences
53+
import com.paulcoding.hviewer.ui.component.ConfirmDialog
4754
import com.paulcoding.hviewer.ui.component.H7Tap
4855
import com.paulcoding.hviewer.ui.component.HBackIcon
56+
import com.paulcoding.hviewer.ui.component.HIcon
57+
import com.paulcoding.hviewer.ui.component.HLoading
4958
import com.paulcoding.hviewer.ui.page.AppViewModel
5059

5160
@OptIn(ExperimentalMaterial3Api::class)
@@ -63,6 +72,8 @@ fun SettingsPage(
6372
val window = (context as ComponentActivity).window
6473
var lockModalVisible by remember { mutableStateOf(false) }
6574
var appLockEnabled by remember { mutableStateOf(Preferences.pin.isNotEmpty()) }
75+
var newVersion by remember { mutableStateOf("") }
76+
var downloadUrl by remember { mutableStateOf("") }
6677
val scrollState = rememberScrollState()
6778

6879
fun onAppLockEnabled(pin: String) {
@@ -147,8 +158,23 @@ fun SettingsPage(
147158
}
148159
}
149160

150-
H7Tap(modifier = Modifier.align(Alignment.CenterHorizontally)) {
151-
appViewModel.setDevMode(it)
161+
Row(
162+
modifier = Modifier.align(Alignment.CenterHorizontally),
163+
verticalAlignment = Alignment.CenterVertically
164+
) {
165+
H7Tap() {
166+
appViewModel.setDevMode(it)
167+
}
168+
HIcon(Icons.Outlined.Update, tint = MaterialTheme.colorScheme.primary) {
169+
appViewModel.checkForUpdate(BuildConfig.VERSION_NAME,
170+
onUpToDate = {
171+
makeToast(R.string.up_to_date)
172+
},
173+
onUpdateAvailable = { version, url ->
174+
newVersion = version
175+
downloadUrl = url
176+
})
177+
}
152178
}
153179
}
154180
}
@@ -169,6 +195,34 @@ fun SettingsPage(
169195
if (lockModalVisible) LockModal(onDismiss = { lockModalVisible = false }) {
170196
onAppLockEnabled(it)
171197
}
198+
199+
ConfirmDialog(
200+
showDialog = newVersion.isNotEmpty(),
201+
title = stringResource(R.string.update_available),
202+
text = newVersion,
203+
confirmColor = MaterialTheme.colorScheme.primary,
204+
confirmText = stringResource(R.string.install_now),
205+
dismissColor = MaterialTheme.colorScheme.onBackground,
206+
onDismiss = {
207+
newVersion = ""
208+
},
209+
onConfirm = {
210+
appViewModel.downloadAndInstallApk(context, downloadUrl)
211+
newVersion = ""
212+
}
213+
)
214+
215+
if (appState.updatingApk) {
216+
Box(
217+
modifier = Modifier
218+
.fillMaxSize()
219+
.background(Color.Black.copy(alpha = 0.5f))
220+
) {
221+
Box(modifier = Modifier.align(Alignment.Center)) {
222+
HLoading()
223+
}
224+
}
225+
}
172226
}
173227

174228
@Composable

0 commit comments

Comments
 (0)