Skip to content

Commit 8458195

Browse files
committed
Work to update apk
1 parent c40210c commit 8458195

File tree

11 files changed

+294
-5
lines changed

11 files changed

+294
-5
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 {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.paulcoding.hviewer.model
22

3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
5+
36
data class Release(
47
val url: String,
58
val id: Int,
@@ -10,4 +13,10 @@ data class Release(
1013

1114
data class Asset(
1215
val browser_download_url: String,
13-
)
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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.paulcoding.hviewer.R
88
import com.paulcoding.hviewer.helper.extractTarGzFromResponseBody
99
import com.paulcoding.hviewer.helper.log
1010
import com.paulcoding.hviewer.helper.readConfigFile
11+
import com.paulcoding.hviewer.model.HRelease
1112
import com.paulcoding.hviewer.model.Release
1213
import com.paulcoding.hviewer.model.SiteConfigs
1314
import com.paulcoding.hviewer.preference.Preferences
@@ -118,17 +119,19 @@ object Github {
118119

119120
suspend fun checkForUpdate(
120121
currentVersion: String,
121-
onUpdateAvailable: (String, String) -> Unit
122-
) {
122+
onUpdateAvailable: ((String, String) -> Unit)? = null
123+
): HRelease? {
123124
val (owner, repo) = parseRepo(BuildConfig.REPO_URL)
124125
val url = "https://api.github.com/repos/${owner}/${repo}/releases/latest"
125126
ktorClient.use { client ->
126127
val jsonObject: Release = client.get(url).body()
127128
val latestVersion = jsonObject.tag_name.substring(1)
128129
val downloadUrl = jsonObject.assets[0].browser_download_url
129130
if (latestVersion != currentVersion) {
130-
onUpdateAvailable(latestVersion, downloadUrl)
131+
onUpdateAvailable?.invoke(latestVersion, downloadUrl)
132+
return HRelease(latestVersion, downloadUrl)
131133
}
134+
return null
132135
}
133136
}
134137

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
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.paulcoding.hviewer.worker
2+
3+
import android.app.Notification
4+
import android.app.NotificationManager
5+
import android.app.PendingIntent
6+
import android.app.Service
7+
import android.content.Context
8+
import android.content.Intent
9+
import android.net.Uri
10+
import android.os.IBinder
11+
import androidx.core.app.NotificationCompat
12+
import androidx.core.content.FileProvider
13+
import com.paulcoding.hviewer.ACTION_INSTALL_APK
14+
import com.paulcoding.hviewer.CHECK_FOR_UPDATE_APK_CHANNEL
15+
import com.paulcoding.hviewer.MainActivity
16+
import com.paulcoding.hviewer.R
17+
import com.paulcoding.hviewer.model.HRelease
18+
import com.paulcoding.hviewer.network.Github
19+
import com.paulcoding.hviewer.ui.page.post.DownloadService
20+
import com.paulcoding.hviewer.ui.page.post.DownloadService.Companion.ACTION_STOP_SERVICE
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.SupervisorJob
24+
import kotlinx.coroutines.launch
25+
import kotlin.coroutines.CoroutineContext
26+
27+
class DownloadApkService : Service() {
28+
private lateinit var notificationManager: NotificationManager
29+
private lateinit var notificationBuilder: NotificationCompat.Builder
30+
31+
private val job = SupervisorJob()
32+
private val coroutineContext: CoroutineContext
33+
get() = Dispatchers.IO + job
34+
35+
private val notificationId = 2
36+
37+
private fun stopService() {
38+
job.cancel()
39+
stopForeground(STOP_FOREGROUND_REMOVE)
40+
stopSelf()
41+
}
42+
43+
44+
override fun onBind(intent: Intent?): IBinder? {
45+
return null
46+
}
47+
48+
override fun onCreate() {
49+
super.onCreate()
50+
notificationManager = getSystemService(NotificationManager::class.java)
51+
}
52+
53+
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
54+
if (intent.action == ACTION_STOP_SERVICE) {
55+
stopService()
56+
return START_NOT_STICKY
57+
}
58+
59+
val release = intent.getParcelableExtra<HRelease>("release")
60+
61+
if (release == null) {
62+
stopSelf()
63+
return START_NOT_STICKY
64+
} else {
65+
startForeground(notificationId, createNotification())
66+
downloadAndInstall(this, release)
67+
}
68+
return START_NOT_STICKY
69+
}
70+
71+
override fun onDestroy() {
72+
job.cancel()
73+
super.onDestroy()
74+
}
75+
76+
77+
private fun downloadAndInstall(context: Context, release: HRelease) {
78+
try {
79+
CoroutineScope(coroutineContext).launch {
80+
Github.downloadApk(context, release.downloadUrl) { file ->
81+
val uri = FileProvider.getUriForFile(
82+
context,
83+
"${context.packageName}.fileprovider",
84+
file
85+
)
86+
showDownloadCompleteNotification(release, uri)
87+
}
88+
}
89+
} catch (e: Exception) {
90+
e.printStackTrace()
91+
showErrorNotification(e.message ?: "Unknown error")
92+
stopSelf()
93+
}
94+
}
95+
96+
private fun showErrorNotification(msg: String) {
97+
notificationBuilder
98+
.setContentTitle(getString(R.string.error))
99+
.setContentText(msg)
100+
.setSmallIcon(android.R.drawable.stat_notify_error)
101+
.setAutoCancel(true)
102+
103+
notificationManager.notify(notificationId, notificationBuilder.build())
104+
}
105+
106+
private fun createNotification(): Notification {
107+
val stopIntent = Intent(this, DownloadService::class.java).apply {
108+
action = ACTION_STOP_SERVICE
109+
}
110+
val stopPendingIntent =
111+
PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
112+
113+
notificationBuilder =
114+
NotificationCompat.Builder(this, CHECK_FOR_UPDATE_APK_CHANNEL)
115+
.setContentTitle(this.getString(R.string.downloading_apk))
116+
.setSmallIcon(android.R.drawable.stat_sys_download)
117+
.setPriority(NotificationCompat.PRIORITY_LOW)
118+
.setProgress(0, 0, true)
119+
.addAction(android.R.drawable.ic_delete, "Cancel", stopPendingIntent)
120+
val notification = notificationBuilder.build()
121+
notificationManager.notify(notificationId, notification)
122+
return notification
123+
}
124+
125+
126+
private fun showDownloadCompleteNotification(release: HRelease, uri: Uri) {
127+
val intent = Intent(this, MainActivity::class.java).apply {
128+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
129+
data = uri
130+
action = ACTION_INSTALL_APK
131+
}
132+
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
133+
notificationBuilder
134+
.setContentTitle(getString(R.string.install_now))
135+
.setContentText(
136+
this.getString(
137+
R.string.version_,
138+
release.version,
139+
)
140+
)
141+
.setSmallIcon(R.mipmap.ic_launcher_foreground)
142+
.setContentIntent(pendingIntent)
143+
.setAutoCancel(true)
144+
145+
notificationManager.notify(notificationId, notificationBuilder.build())
146+
147+
val completedNotification =
148+
NotificationCompat.Builder(this, CHECK_FOR_UPDATE_APK_CHANNEL)
149+
.setContentTitle(getString(R.string.download_complete))
150+
.setContentText(getString(R.string.tap_to_open))
151+
.setSmallIcon(android.R.drawable.stat_sys_download_done)
152+
.setContentIntent(pendingIntent)
153+
.setAutoCancel(true)
154+
.build()
155+
156+
notificationManager.notify(notificationId, completedNotification)
157+
}
158+
}

app/src/main/java/com/paulcoding/hviewer/worker/Schedule.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ fun scheduleScriptsUpdate(context: Context) {
2525
)
2626
}
2727

28+
fun scheduleApkUpdate(context: Context) {
29+
val constraints = Constraints.Builder()
30+
.setRequiredNetworkType(NetworkType.CONNECTED)
31+
.build()
32+
val updateScriptsWorkRequest =
33+
PeriodicWorkRequestBuilder<UpdateApkWorker>(1, TimeUnit.DAYS)
34+
.setConstraints(constraints)
35+
.setInitialDelay(calculateDelayUntilMidnight(), TimeUnit.MILLISECONDS)
36+
.build()
37+
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
38+
"updateApk",
39+
ExistingPeriodicWorkPolicy.KEEP,
40+
updateScriptsWorkRequest
41+
)
42+
}
43+
2844
fun calculateDelayUntilMidnight(): Long {
2945
val now = Calendar.getInstance()
3046
val midnight = Calendar.getInstance().apply {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.paulcoding.hviewer.worker
2+
3+
import android.app.NotificationManager
4+
import android.app.PendingIntent
5+
import android.content.Context
6+
import android.content.Intent
7+
import androidx.core.app.NotificationCompat
8+
import androidx.work.CoroutineWorker
9+
import androidx.work.Data
10+
import androidx.work.WorkerParameters
11+
import com.paulcoding.hviewer.BuildConfig
12+
import com.paulcoding.hviewer.CHECK_FOR_UPDATE_APK_CHANNEL
13+
import com.paulcoding.hviewer.R
14+
import com.paulcoding.hviewer.model.HRelease
15+
import com.paulcoding.hviewer.network.Github
16+
17+
class UpdateApkWorker(
18+
val context: Context,
19+
workerParams: WorkerParameters
20+
) : CoroutineWorker(context, workerParams) {
21+
private val notificationId = 2
22+
23+
override suspend fun doWork(): Result {
24+
try {
25+
val hRelease = Github.checkForUpdate(BuildConfig.VERSION_NAME)
26+
if (hRelease != null) {
27+
notify(context, hRelease)
28+
}
29+
return Result.success()
30+
} catch (e: Exception) {
31+
e.printStackTrace()
32+
val data = Data.Builder().putString("error", e.message).build()
33+
return Result.failure(data)
34+
}
35+
}
36+
37+
private fun notify(context: Context, release: HRelease) {
38+
val intent = Intent(context, DownloadApkService::class.java).apply {
39+
putExtra("release", release)
40+
}
41+
42+
val pendingIntent =
43+
PendingIntent.getService(
44+
context,
45+
0,
46+
intent,
47+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
48+
)
49+
50+
val notificationManager = context.getSystemService(NotificationManager::class.java)
51+
val notificationBuilder = NotificationCompat.Builder(context, CHECK_FOR_UPDATE_APK_CHANNEL)
52+
.setContentTitle(context.getString(R.string.new_version_available))
53+
.setContentText(
54+
context.getString(
55+
R.string.version_,
56+
release.version,
57+
)
58+
)
59+
.setSmallIcon(R.mipmap.ic_launcher_foreground)
60+
.addAction(
61+
android.R.drawable.ic_media_play,
62+
context.getString(R.string.install), pendingIntent
63+
)
64+
65+
.setAutoCancel(false)
66+
67+
notificationManager.notify(notificationId, notificationBuilder.build())
68+
}
69+
}

0 commit comments

Comments
 (0)