Skip to content

Commit 41c7dd0

Browse files
committed
feat: Add app update check
Signed-off-by: Hu Shenghao <dede.hu@qq.com>
1 parent 8fe1490 commit 41c7dd0

File tree

20 files changed

+297
-16
lines changed

20 files changed

+297
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Add Android L Preview Easter Egg [#714](https://github.com/hushenghao/AndroidEasterEggs/issues/714)
66
- Add Platlogo snapshot preview
7+
- Add app update check
78
- Timeline add Android logo
89
- Optimization Cat Editor options panel
910
- Fix Timeline list scroll gesture conflicts [#654](https://github.com/hushenghao/AndroidEasterEggs/issues/654)

CHANGELOG_zh.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- 新增 Android L 预览版彩蛋 [#714](https://github.com/hushenghao/AndroidEasterEggs/issues/714)
66
- 新增 Platlogo 快照预览
7+
- 新增 App 更新检查
78
- 时间线添加 Android logo
89
- 优化 Cat Editor 选项面板
910
- 修复时间线列表滚动手势冲突 [#654](https://github.com/hushenghao/AndroidEasterEggs/issues/654)

app/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.dede.android_eggs.dls.marketImplementation
44

55
plugins {
66
id("easter.eggs.app")
7+
alias(libs.plugins.kotlin.serialization)
78
alias(libs.plugins.aboutlibraries.android)
89
}
910

@@ -99,6 +100,11 @@ dependencies {
99100
implementation(libs.squareup.okio)
100101
implementation(libs.blurhash.android)
101102
debugImplementation(libs.squareup.leakcanary)
103+
implementation(libs.ktor.core)
104+
implementation(libs.ktor.android)
105+
implementation(libs.ktor.content.negotiation)
106+
implementation(libs.ktor.logging)
107+
implementation(libs.ktor.json)
102108

103109
implementation(project(":core:local-provider"))
104110
implementation(project(":core:navigation"))
@@ -140,6 +146,7 @@ dependencies {
140146
implementation(project(":eggs:Base"))
141147

142148
marketImplementation(libs.google.play.review)
149+
marketImplementation(libs.google.play.update)
143150

144151
testImplementation(libs.junit)
145152
androidTestImplementation(libs.nanohttpd)
Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,97 @@
11
package com.dede.android_eggs
22

3+
import android.app.Activity
4+
import android.util.Log
35
import androidx.activity.ComponentActivity
4-
import com.dede.android_eggs.inject.FlavorFeatures
6+
import com.dede.android_eggs.flavor.FlavorFeatures
7+
import com.dede.android_eggs.flavor.LatestRelease
8+
import io.ktor.client.HttpClient
9+
import io.ktor.client.call.NoTransformationFoundException
10+
import io.ktor.client.call.body
11+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
12+
import io.ktor.client.plugins.logging.LogLevel
13+
import io.ktor.client.plugins.logging.Logger
14+
import io.ktor.client.plugins.logging.Logging
15+
import io.ktor.client.request.get
16+
import io.ktor.client.request.headers
17+
import io.ktor.client.request.url
18+
import io.ktor.http.HttpHeaders.Accept
19+
import io.ktor.http.HttpStatusCode.Companion.OK
20+
import io.ktor.serialization.kotlinx.json.json
21+
import kotlinx.serialization.SerialName
22+
import kotlinx.serialization.Serializable
23+
import kotlinx.serialization.json.Json
524

625
class FlavorFeaturesImpl : FlavorFeatures {
7-
override fun call(activity: ComponentActivity) {
26+
override fun launchReview(activity: ComponentActivity) {
827
}
9-
}
28+
29+
override suspend fun checkUpdate(activity: Activity): LatestRelease? {
30+
val client = HttpClient {
31+
install(Logging) {
32+
logger = object : Logger {
33+
override fun log(message: String) {
34+
Log.d("KtorClient", message)
35+
}
36+
}
37+
level = LogLevel.HEADERS
38+
}
39+
install(ContentNegotiation) {
40+
json(Json {
41+
ignoreUnknownKeys = true
42+
})
43+
}
44+
}
45+
try {
46+
val response = client.get {
47+
url("https://api.github.com/repos/hushenghao/AndroidEasterEggs/releases/latest")
48+
headers {
49+
append(Accept, "application/vnd.github+json")
50+
append("X-GitHub-Api-Version", "2022-11-28")
51+
}
52+
}
53+
if (response.status != OK) {
54+
return null
55+
}
56+
return response.body<GitHubLatestRelease>().toLatestRelease()
57+
} catch (e: NoTransformationFoundException) {
58+
return null
59+
} finally {
60+
client.close()
61+
}
62+
}
63+
}
64+
65+
@Serializable
66+
private data class GitHubLatestRelease(
67+
@SerialName("html_url")
68+
val htmlUrl: String,
69+
@SerialName("tag_name")
70+
val tagName: String,
71+
val assets: List<Asset>,
72+
@SerialName("body")
73+
val changelog: String,
74+
) {
75+
@Serializable
76+
data class Asset(
77+
@SerialName("browser_download_url")
78+
val downloadUrl: String,
79+
val name: String,
80+
@SerialName("content_type")
81+
val contentType: String,
82+
)
83+
84+
fun toLatestRelease(): LatestRelease? {
85+
val apkAsset =
86+
assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }
87+
if (apkAsset == null) {
88+
return null
89+
}
90+
return LatestRelease(
91+
versionName = tagName,
92+
pageUrl = htmlUrl,
93+
downloadUrl = apkAsset.downloadUrl,
94+
changelog = changelog,
95+
)
96+
}
97+
}

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
xmlns:tools="http://schemas.android.com/tools"
55
dede:once="🍃">
66

7+
<uses-permission android:name="android.permission.INTERNET"/>
8+
79
<uses-permission
810
android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
911
android:maxSdkVersion="26" />

app/src/main/java/com/dede/android_eggs/inject/FlavorFeatures.kt renamed to app/src/main/java/com/dede/android_eggs/flavor/FlavorFeatures.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
package com.dede.android_eggs.inject
1+
package com.dede.android_eggs.flavor
22

3+
import android.app.Activity
34
import androidx.activity.ComponentActivity
45
import com.dede.android_eggs.FlavorFeaturesImpl
56

@@ -14,5 +15,7 @@ interface FlavorFeatures {
1415
}
1516
}
1617

17-
fun call(activity: ComponentActivity)
18+
fun launchReview(activity: ComponentActivity)
19+
20+
suspend fun checkUpdate(activity: Activity): LatestRelease?
1821
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.dede.android_eggs.flavor
2+
3+
data class LatestRelease(
4+
val versionName: String,
5+
val changelog: String,
6+
val pageUrl: String,
7+
val downloadUrl: String,
8+
)

app/src/main/java/com/dede/android_eggs/views/main/EasterEggsActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import androidx.activity.enableEdgeToEdge
88
import androidx.appcompat.app.AppCompatActivity
99
import androidx.core.net.toUri
1010
import com.dede.android_eggs.R
11-
import com.dede.android_eggs.inject.FlavorFeatures
11+
import com.dede.android_eggs.flavor.FlavorFeatures
1212
import com.dede.android_eggs.util.setupSplashScreen
1313
import com.dede.android_eggs.views.main.util.EasterEggShortcutsHelp
1414
import com.dede.android_eggs.views.main.util.IntentHandler
@@ -43,7 +43,7 @@ class EasterEggsActivity : AppCompatActivity() {
4343
EasterEggShortcutsHelp.updateShortcuts(this, pureEasterEggs)
4444

4545
// call flavor features
46-
FlavorFeatures.get().call(this)
46+
FlavorFeatures.get().launchReview(this)
4747
}
4848

4949
override fun onNewIntent(intent: Intent) {

app/src/main/java/com/dede/android_eggs/views/settings/SettingsScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ fun SettingsScreen(drawerState: DrawerState = rememberDrawerState(DrawerValue.Cl
156156

157157
SettingDivider()
158158

159-
ContributeGroup()
160-
161159
AboutGroup()
162160

161+
ContributeGroup()
162+
163163
ContactMeGroup()
164164
}
165165
}

app/src/main/java/com/dede/android_eggs/views/settings/compose/options/VersionOption.kt

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,59 @@
1+
@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2+
13
package com.dede.android_eggs.views.settings.compose.options
24

5+
import androidx.activity.compose.LocalActivity
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.Spacer
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.width
311
import androidx.compose.material.icons.Icons
12+
import androidx.compose.material.icons.automirrored.rounded.NavigateNext
413
import androidx.compose.material.icons.outlined.NewReleases
14+
import androidx.compose.material.icons.rounded.Upgrade
15+
import androidx.compose.material3.AlertDialog
16+
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
17+
import androidx.compose.material3.FilledTonalIconButton
18+
import androidx.compose.material3.Icon
19+
import androidx.compose.material3.IconButtonShapes
20+
import androidx.compose.material3.MaterialShapes
21+
import androidx.compose.material3.Text
22+
import androidx.compose.material3.TextButton
23+
import androidx.compose.material3.toShape
524
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableStateOf
27+
import androidx.compose.runtime.remember
28+
import androidx.compose.runtime.rememberCoroutineScope
29+
import androidx.compose.runtime.setValue
30+
import androidx.compose.ui.Alignment
31+
import androidx.compose.ui.Modifier
632
import androidx.compose.ui.platform.LocalContext
733
import androidx.compose.ui.res.stringResource
34+
import androidx.compose.ui.unit.dp
835
import androidx.core.net.toUri
936
import com.dede.android_eggs.BuildConfig
1037
import com.dede.android_eggs.R
38+
import com.dede.android_eggs.flavor.FlavorFeatures
39+
import com.dede.android_eggs.flavor.LatestRelease
1140
import com.dede.android_eggs.util.AGPUtils
1241
import com.dede.android_eggs.util.CustomTabsBrowser
42+
import com.dede.android_eggs.util.compareStringVersion
43+
import com.dede.android_eggs.views.main.compose.isAgreedPrivacyPolicy
1344
import com.dede.android_eggs.views.settings.compose.basic.Option
1445
import com.dede.android_eggs.views.settings.compose.basic.OptionShapes
1546
import com.dede.android_eggs.views.settings.compose.basic.imageVectorIconBlock
47+
import com.dede.basic.toast
48+
import kotlinx.coroutines.launch
49+
import com.dede.android_eggs.resources.R as StringR
1650

1751

1852
@Composable
1953
fun VersionOption() {
2054
val context = LocalContext.current
55+
56+
var newRelease: LatestRelease? by remember { mutableStateOf(null) }
2157
Option(
2258
shape = OptionShapes.firstShape(),
2359
leadingIcon = imageVectorIconBlock(imageVector = Icons.Outlined.NewReleases),
@@ -27,14 +63,87 @@ fun VersionOption() {
2763
BuildConfig.VERSION_CODE
2864
),
2965
desc = AGPUtils.getVcsRevision(7),
66+
trailingContent = {
67+
if (isAgreedPrivacyPolicy(context)) {
68+
val activity = LocalActivity.current
69+
val coroutineScope = rememberCoroutineScope()
70+
FilledTonalIconButton(
71+
shapes = IconButtonShapes(MaterialShapes.SoftBurst.toShape()),
72+
onClick = onClick@{
73+
if (activity == null) {
74+
return@onClick
75+
}
76+
coroutineScope.launch {
77+
val latestRelease = FlavorFeatures.get().checkUpdate(activity)
78+
if (latestRelease != null) {
79+
if (compareStringVersion(
80+
latestRelease.versionName,
81+
BuildConfig.VERSION_NAME
82+
) > 0
83+
) {
84+
newRelease = latestRelease
85+
} else {
86+
context.toast(StringR.string.toast_no_update_found)
87+
}
88+
}
89+
}
90+
}
91+
) {
92+
Icon(imageVector = Icons.Rounded.Upgrade, contentDescription = null)
93+
}
94+
} else {
95+
Icon(
96+
imageVector = Icons.AutoMirrored.Rounded.NavigateNext,
97+
contentDescription = null
98+
)
99+
}
100+
},
30101
onClick = {
31102
val revision = AGPUtils.getVcsRevision()
32103
val uri = if (revision == null) {
33104
context.getString(R.string.url_github)
34105
} else {
35106
context.getString(R.string.url_github_commit, revision)
36107
}
37-
com.dede.android_eggs.util.CustomTabsBrowser.launchUrl(context, uri.toUri())
108+
CustomTabsBrowser.launchUrl(context, uri)
38109
}
39110
)
111+
112+
if (newRelease != null) {
113+
val release = newRelease!!
114+
AlertDialog(
115+
onDismissRequest = { newRelease = null },
116+
title = {
117+
Row(
118+
modifier = Modifier.fillMaxWidth(),
119+
verticalAlignment = Alignment.CenterVertically,
120+
horizontalArrangement = Arrangement.Center,
121+
) {
122+
Icon(
123+
imageVector = Icons.Outlined.NewReleases,
124+
contentDescription = null,
125+
)
126+
Spacer(modifier = Modifier.width(10.dp))
127+
Text(release.versionName)
128+
}
129+
},
130+
text = {
131+
Text(release.changelog)
132+
},
133+
confirmButton = {
134+
TextButton(
135+
onClick = {
136+
CustomTabsBrowser.launchUrl(context, release.downloadUrl)
137+
}
138+
) {
139+
Text(stringResource(StringR.string.label_update))
140+
}
141+
},
142+
dismissButton = {
143+
TextButton(onClick = { newRelease = null }) {
144+
Text(stringResource(android.R.string.cancel))
145+
}
146+
},
147+
)
148+
}
40149
}

0 commit comments

Comments
 (0)