feat: permission screen

- single context coroutine scope
- refactor activity launcher helper
- move updater to home section
This commit is contained in:
rhunk 2023-09-01 18:15:40 +02:00
parent a3edd40cfb
commit 6b9938b8b2
15 changed files with 299 additions and 188 deletions

View File

@ -14,6 +14,7 @@ import coil.ImageLoader
import coil.decode.VideoFrameDecoder
import coil.disk.DiskCache
import coil.memory.MemoryCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import me.rhunk.snapenhance.bridge.BridgeService
import me.rhunk.snapenhance.core.BuildConfig
@ -71,6 +72,7 @@ class RemoteSideContext(
}
.components { add(VideoFrameDecoder.Factory()) }.build()
}
val coroutineScope = CoroutineScope(Dispatchers.IO)
fun reload() {
runCatching {
@ -103,7 +105,7 @@ class RemoteSideContext(
)
},
modInfo = ModInfo(
loaderPackageName = MainActivity::class.java.`package`?.name ?: "unknown",
loaderPackageName = MainActivity::class.java.`package`?.name,
buildPackageName = BuildConfig.APPLICATION_ID,
buildVersion = BuildConfig.VERSION_NAME,
buildVersionCode = BuildConfig.VERSION_CODE.toLong(),
@ -119,7 +121,6 @@ class RemoteSideContext(
),
platformInfo = PlatformInfo(
device = Build.DEVICE,
buildFingerprint = Build.FINGERPRINT,
androidVersion = Build.VERSION.RELEASE,
systemAbi = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"
)

View File

@ -316,7 +316,7 @@ class DownloadProcessor (
}
fun onReceive(intent: Intent) {
CoroutineScope(Dispatchers.IO).launch {
remoteSideContext.coroutineScope.launch {
val downloadMetadata = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_METADATA_EXTRA)!!, DownloadMetadata::class.java)
val downloadRequest = gson.fromJson(intent.getStringExtra(DownloadManagerClient.DOWNLOAD_REQUEST_EXTRA)!!, DownloadRequest::class.java)

View File

@ -9,8 +9,6 @@ import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.toBitmap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.R
import me.rhunk.snapenhance.RemoteSideContext
@ -26,8 +24,6 @@ class StreaksReminder(
private const val NOTIFICATION_CHANNEL_ID = "streaks"
}
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private fun getNotificationManager(context: Context) = context.getSystemService(NotificationManager::class.java).apply {
createNotificationChannel(
NotificationChannel(
@ -65,7 +61,7 @@ class StreaksReminder(
}
notifyFriendList.forEach { (streaks, friend) ->
coroutineScope.launch {
remoteSideContext.coroutineScope.launch {
val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
val bitmojiImage = remoteSideContext.imageLoader.execute(
ImageRequestHelper.newBitmojiImageRequest(ctx, bitmojiUrl)

View File

@ -10,7 +10,7 @@ data class SnapchatAppInfo(
)
data class ModInfo(
val loaderPackageName: String,
val loaderPackageName: String?,
val buildPackageName: String,
val buildVersion: String,
val buildVersionCode: Long,
@ -22,7 +22,6 @@ data class ModInfo(
data class PlatformInfo(
val device: String,
val buildFingerprint: String,
val androidVersion: String,
val systemAbi: String,
)

View File

@ -0,0 +1,31 @@
package me.rhunk.snapenhance.ui.manager.data
import com.google.gson.JsonParser
import me.rhunk.snapenhance.core.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.Request
object Updater {
data class LatestRelease(
val versionName: String,
val releaseUrl: String
)
fun checkForLatestRelease(): LatestRelease? {
val endpoint = Request.Builder().url("https://api.github.com/repos/rhunk/SnapEnhance/releases").build()
val response = OkHttpClient().newCall(endpoint).execute()
if (!response.isSuccessful) throw Throwable("Failed to fetch releases: ${response.code}")
val releases = JsonParser.parseString(response.body.string()).asJsonArray.also {
if (it.size() == 0) throw Throwable("No releases found")
}
val latestRelease = releases.get(0).asJsonObject
val latestVersion = latestRelease.getAsJsonPrimitive("tag_name").asString
if (latestVersion.removePrefix("v") == BuildConfig.VERSION_NAME) return null
return LatestRelease(latestVersion, endpoint.url.toString().replace("api.", "").replace("repos/", ""))
}
}

View File

@ -50,7 +50,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.launch
@ -64,11 +63,10 @@ import me.rhunk.snapenhance.ui.util.ImageRequestHelper
class DownloadsSection : Section() {
private val loadedDownloads = mutableStateOf(mapOf<Int, DownloadObject>())
private var currentFilter = mutableStateOf(MediaDownloadSource.NONE)
private val coroutineScope = CoroutineScope(Dispatchers.IO)
override fun onResumed() {
super.onResumed()
coroutineScope.launch {
context.coroutineScope.launch {
loadByFilter(currentFilter.value)
}
}
@ -129,7 +127,7 @@ class DownloadsSection : Section() {
}
},
onClick = {
coroutineScope.launch {
context.coroutineScope.launch {
loadByFilter(filter)
showMenu = false
}

View File

@ -1,5 +1,6 @@
package me.rhunk.snapenhance.ui.manager.sections.home
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
@ -48,9 +49,11 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.R
import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
import me.rhunk.snapenhance.ui.manager.data.Updater
import me.rhunk.snapenhance.ui.setup.Requirements
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
import me.rhunk.snapenhance.ui.util.saveFile
@ -64,9 +67,10 @@ class HomeSection : Section() {
const val LOGS_SECTION_ROUTE = "home_logs"
}
private val installationSummary = mutableStateOf(null as InstallationSummary?)
private val userLocale = mutableStateOf(null as String?)
private var installationSummary: InstallationSummary? = null
private var userLocale: String? = null
private val homeSubSection by lazy { HomeSubSection(context) }
private var latestUpdate: Updater.LatestRelease? = null
private lateinit var activityLauncherHelper: ActivityLauncherHelper
override fun init() {
@ -100,42 +104,16 @@ class HomeSection : Section() {
@Composable
private fun SummaryCards(installationSummary: InstallationSummary) {
OutlinedCard(
modifier = Modifier
.padding(all = cardMargin)
.fillMaxWidth()
) {
SummaryCardRow(
icon = Icons.Filled.Map,
title = if (installationSummary.modInfo == null || installationSummary.modInfo.mappingsOutdated == true) {
"Mappings ${if (installationSummary.modInfo == null) "not generated" else "outdated"}"
} else {
"Mappings version ${installationSummary.modInfo.mappingVersion}"
}
) {
Button(onClick = {
context.checkForRequirements(Requirements.MAPPINGS)
}, modifier = Modifier.height(40.dp)) {
Icon(Icons.Filled.Refresh, contentDescription = null)
}
}
SummaryCardRow(icon = Icons.Filled.Language, title = userLocale.value ?: "Unknown") {
Button(onClick = {
context.checkForRequirements(Requirements.LANGUAGE)
}, modifier = Modifier.height(40.dp)) {
Icon(Icons.Filled.OpenInNew, contentDescription = null)
}
}
}
val summaryInfo = remember {
mapOf(
"Build Issuer" to (installationSummary.modInfo?.buildIssuer ?: "Unknown"),
"Build Type" to (if (installationSummary.modInfo?.isDebugBuild == true) "debug" else "release"),
"Build Version" to (installationSummary.modInfo?.buildVersion ?: "Unknown"),
"Build Package" to (installationSummary.modInfo?.buildPackageName ?: "Unknown"),
"Activity Package" to (installationSummary.modInfo?.loaderPackageName ?: "Unknown"),
"Device" to installationSummary.platformInfo.device,
"Android version" to installationSummary.platformInfo.androidVersion,
"System ABI" to installationSummary.platformInfo.systemAbi,
"Build fingerprint" to installationSummary.platformInfo.buildFingerprint
"Android Version" to installationSummary.platformInfo.androidVersion,
"System ABI" to installationSummary.platformInfo.systemAbi
)
}
@ -172,7 +150,35 @@ class HomeSection : Section() {
}
}
}
}
OutlinedCard(
modifier = Modifier
.padding(all = cardMargin)
.fillMaxWidth()
) {
SummaryCardRow(
icon = Icons.Filled.Map,
title = if (installationSummary.modInfo == null || installationSummary.modInfo.mappingsOutdated == true) {
"Mappings ${if (installationSummary.modInfo == null) "not generated" else "outdated"}"
} else {
"Mappings version ${installationSummary.modInfo.mappingVersion}"
}
) {
Button(onClick = {
context.checkForRequirements(Requirements.MAPPINGS)
}, modifier = Modifier.height(40.dp)) {
Icon(Icons.Filled.Refresh, contentDescription = null)
}
}
SummaryCardRow(icon = Icons.Filled.Language, title = userLocale ?: "Unknown") {
Button(onClick = {
context.checkForRequirements(Requirements.LANGUAGE)
}, modifier = Modifier.height(40.dp)) {
Icon(Icons.Filled.OpenInNew, contentDescription = null)
}
}
}
}
@ -180,8 +186,19 @@ class HomeSection : Section() {
if (!context.mappings.isMappingsLoaded()) {
context.mappings.init(context.androidContext)
}
installationSummary.value = context.installationSummary
userLocale.value = context.translation.loadedLocale.getDisplayName(Locale.getDefault())
context.coroutineScope.launch {
userLocale = context.translation.loadedLocale.getDisplayName(Locale.getDefault())
runCatching {
installationSummary = context.installationSummary
}.onFailure {
context.longToast("SnapEnhance failed to load installation summary: ${it.message}")
}
runCatching {
latestUpdate = Updater.checkForLatestRelease()
}.onFailure {
context.longToast("SnapEnhance failed to check for updates: ${it.message}")
}
}
}
override fun sectionTopBarName(): String {
@ -304,13 +321,53 @@ class HomeSection : Section() {
)
}
if (latestUpdate != null) {
OutlinedCard(
modifier = Modifier
.padding(all = cardMargin)
.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
){
Row(
modifier = Modifier
.fillMaxWidth()
.padding(all = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = "SnapEnhance Update",
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
)
Text(
fontSize = 12.sp,
text = "Version ${latestUpdate?.versionName} is available!",
lineHeight = 20.sp
)
}
Button(onClick = {
context.activity?.startActivity(
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(latestUpdate?.releaseUrl)
}
)
}, modifier = Modifier.height(40.dp)) {
Text(text = "Download")
}
}
}
}
Text(
text = "An xposed module that enhances the Snapchat experience",
modifier = Modifier.padding(16.dp)
)
SummaryCards(installationSummary = installationSummary.value ?: return)
SummaryCards(installationSummary = installationSummary ?: return)
}
}
}

View File

@ -5,6 +5,7 @@ object Requirements {
const val LANGUAGE = 0b00010
const val MAPPINGS = 0b00100
const val SAVE_FOLDER = 0b01000
const val GRANT_PERMISSIONS = 0b10000
fun getName(requirement: Int): String {
return when (requirement) {
@ -12,6 +13,7 @@ object Requirements {
LANGUAGE -> "LANGUAGE"
MAPPINGS -> "MAPPINGS"
SAVE_FOLDER -> "SAVE_FOLDER"
GRANT_PERMISSIONS -> "GRANT_PERMISSIONS"
else -> "UNKNOWN"
}
}

View File

@ -36,6 +36,7 @@ import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.ui.AppMaterialTheme
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.PermissionsScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.PickLanguageScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen
@ -65,6 +66,9 @@ class SetupActivity : ComponentActivity() {
if (isFirstRun || hasRequirement(Requirements.LANGUAGE)) {
add(PickLanguageScreen().apply { route = "language" })
}
if (isFirstRun || hasRequirement(Requirements.GRANT_PERMISSIONS)) {
add(PermissionsScreen().apply { route = "permissions" })
}
if (isFirstRun || hasRequirement(Requirements.SAVE_FOLDER)) {
add(SaveFolderScreen().apply { route = "saveFolder" })
}

View File

@ -0,0 +1,115 @@
package me.rhunk.snapenhance.ui.setup.screens.impl
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
class PermissionsScreen : SetupScreen() {
private lateinit var activityLauncherHelper: ActivityLauncherHelper
override fun init() {
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
}
@SuppressLint("BatteryLife")
@Composable
override fun Content() {
var notificationPermissionGranted by remember { mutableStateOf(true) }
var isBatteryOptimisationIgnored by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionGranted = context.androidContext.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
}
val powerManager = context.androidContext.getSystemService(Context.POWER_SERVICE) as PowerManager
isBatteryOptimisationIgnored = powerManager.isIgnoringBatteryOptimizations(context.androidContext.packageName)
}
if (isBatteryOptimisationIgnored && notificationPermissionGranted) {
allowNext(true)
} else {
allowNext(false)
}
DialogText(text = "To continue you need to fit the following requirements:")
OutlinedCard(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
) {
Column(
modifier = Modifier
.padding(5.dp)
) {
Row(
horizontalArrangement = Arrangement.Absolute.SpaceAround
) {
DialogText(text = "Notification access", modifier = Modifier.weight(1f))
if (notificationPermissionGranted) {
DialogText(text = "Granted")
} else {
Button(onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activityLauncherHelper.requestPermission(Manifest.permission.POST_NOTIFICATIONS) { resultCode, _ ->
coroutineScope.launch {
notificationPermissionGranted = resultCode == ComponentActivity.RESULT_OK
}
}
}
}) {
Text(text = "Request")
}
}
}
Row {
DialogText(text = "Battery optimisation", modifier = Modifier.weight(1f))
if (isBatteryOptimisationIgnored) {
DialogText(text = "Ignored")
} else {
Button(onClick = {
activityLauncherHelper.launch(Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.androidContext.packageName}")
}) { resultCode, _ ->
coroutineScope.launch {
isBatteryOptimisationIgnored = resultCode == 0
}
}
}) {
Text(text = "Request")
}
}
}
}
}
}
}

View File

@ -6,29 +6,47 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import me.rhunk.snapenhance.Logger
typealias ActivityLauncherCallback = (resultCode: Int, intent: Intent?) -> Unit
class ActivityLauncherHelper(
val activity: ComponentActivity
val activity: ComponentActivity,
) {
private var callback: ((Intent) -> Unit)? = null
private var activityResultLauncher: ActivityResultLauncher<Intent> =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == ComponentActivity.RESULT_OK) {
runCatching {
callback?.let { it(result.data!!) }
}.onFailure {
Logger.directError("Failed to process activity result", it)
}
private var callback: ActivityLauncherCallback? = null
private var permissionResultLauncher: ActivityResultLauncher<String> =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
runCatching {
callback?.let { it(if (result) ComponentActivity.RESULT_OK else ComponentActivity.RESULT_CANCELED, null) }
}.onFailure {
Logger.directError("Failed to process activity result", it)
}
callback = null
}
fun launch(intent: Intent, callback: (Intent) -> Unit) {
private var activityResultLauncher: ActivityResultLauncher<Intent> =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
runCatching {
callback?.let { it(result.resultCode, result.data) }
}.onFailure {
Logger.directError("Failed to process activity result", it)
}
callback = null
}
fun launch(intent: Intent, callback: ActivityLauncherCallback) {
if (this.callback != null) {
throw IllegalStateException("Already launching an activity")
}
this.callback = callback
activityResultLauncher.launch(intent)
}
fun requestPermission(permission: String, callback: ActivityLauncherCallback) {
if (this.callback != null) {
throw IllegalStateException("Already launching an activity")
}
this.callback = callback
permissionResultLauncher.launch(permission)
}
}
fun ActivityLauncherHelper.chooseFolder(callback: (uri: String) -> Unit) {
@ -36,8 +54,11 @@ fun ActivityLauncherHelper.chooseFolder(callback: (uri: String) -> Unit) {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) {
val uri = it.data ?: return@launch
) { resultCode, intent ->
if (resultCode != ComponentActivity.RESULT_OK) {
return@launch
}
val uri = intent?.data ?: return@launch
val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission(
uri,
@ -55,8 +76,11 @@ fun ActivityLauncherHelper.saveFile(name: String, type: String = "*/*", callback
.putExtra(Intent.EXTRA_TITLE, name)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) {
val uri = it.data ?: return@launch
) { resultCode, intent ->
if (resultCode != ComponentActivity.RESULT_OK) {
return@launch
}
val uri = intent?.data ?: return@launch
val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission(
uri,
@ -72,8 +96,11 @@ fun ActivityLauncherHelper.openFile(type: String = "*/*", callback: (uri: String
.setType(type)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) {
val uri = it.data ?: return@launch
) { resultCode, intent ->
if (resultCode != ComponentActivity.RESULT_OK) {
return@launch
}
val uri = intent?.data ?: return@launch
val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
callback(value)

View File

@ -101,7 +101,6 @@ class AlertDialogs(
Text(
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
)
if (message != null) {

View File

@ -2,11 +2,9 @@ package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.FeatureNotice
import me.rhunk.snapenhance.data.NotificationType
class Global : ConfigContainer() {
val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.MAY_BAN) }
val autoUpdater = unique("auto_updater", "EVERY_LAUNCH", "DAILY", "WEEKLY").apply { set("DAILY") }
val disableMetrics = boolean("disable_metrics")
val blockAds = boolean("block_ads")
val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") { addNotices(FeatureNotice.MAY_BAN) }

View File

@ -1,114 +0,0 @@
package me.rhunk.snapenhance.features.impl
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import com.google.gson.JsonParser
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import okhttp3.OkHttpClient
import okhttp3.Request
class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
override fun asyncOnActivityCreate() {
val autoUpdaterTime = context.config.global.autoUpdater.getNullable() ?: return
val currentTimeMillis = System.currentTimeMillis()
val checkForUpdatesTimestamp = context.bridgeClient.getAutoUpdaterTime()
val delayTimestamp = when (autoUpdaterTime) {
"EVERY_LAUNCH" -> currentTimeMillis - checkForUpdatesTimestamp
"DAILY" -> 86400000L
"WEEKLY" -> 604800000L
else -> return
}
if (checkForUpdatesTimestamp + delayTimestamp > currentTimeMillis) return
runCatching {
checkForUpdates()
}.onFailure {
context.log.error("Failed to check for updates: ${it.message}", it)
}.onSuccess {
context.bridgeClient.setAutoUpdaterTime(currentTimeMillis)
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun checkForUpdates(): String? {
val endpoint = Request.Builder().url("https://api.github.com/repos/rhunk/SnapEnhance/releases").build()
val response = OkHttpClient().newCall(endpoint).execute()
if (!response.isSuccessful) throw Throwable("Failed to fetch releases: ${response.code}")
val releases = JsonParser.parseString(response.body.string()).asJsonArray.also {
if (it.size() == 0) throw Throwable("No releases found")
}
val latestRelease = releases.get(0).asJsonObject
val latestVersion = latestRelease.getAsJsonPrimitive("tag_name").asString
if (latestVersion.removePrefix("v") == BuildConfig.VERSION_NAME) return null
val architectureName = Build.SUPPORTED_ABIS.let {
if (it.contains("arm64-v8a")) return@let "armv8"
if (it.contains("armeabi-v7a") || it.contains("armeabi")) return@let "armv7"
throw Throwable("Failed getting architecture")
}
val releaseContentBody = latestRelease.getAsJsonPrimitive("body").asString
val downloadEndpoint = "https://github.com/rhunk/SnapEnhance/releases/download/${latestVersion}/app-${latestVersion.removePrefix("v")}-${architectureName}-release-signed.apk"
context.runOnUiThread {
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
.setTitle(context.translation["auto_updater.dialog_title"])
.setMessage(
context.translation.format("auto_updater.dialog_message",
"version" to latestVersion,
"body" to releaseContentBody)
)
.setNegativeButton(context.translation["auto_updater.dialog_negative_button"]) { dialog, _ ->
dialog.dismiss()
}
.setPositiveButton(context.translation["auto_updater.dialog_positive_button"]) { dialog, _ ->
dialog.dismiss()
context.longToast(context.translation["auto_updater.downloading_toast"])
val request = DownloadManager.Request(Uri.parse(downloadEndpoint))
.setTitle(context.translation["auto_updater.download_manager_notification_title"])
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "latest-snapenhance.apk")
.setMimeType("application/vnd.android.package-archive")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
val downloadManager = context.androidContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadId = downloadManager.enqueue(request)
val onCompleteReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id != downloadId) return
context.unregisterReceiver(this)
context.startActivity(
Intent(Intent.ACTION_VIEW).apply {
setDataAndType(downloadManager.getUriForDownloadedFile(downloadId), "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
)
}
}
context.mainActivity?.registerReceiver(onCompleteReceiver, IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE
))
}.show()
}
return latestVersion
}
}

View File

@ -4,7 +4,6 @@ import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.impl.AutoUpdater
import me.rhunk.snapenhance.features.impl.ConfigurationOverride
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
@ -27,11 +26,11 @@ import me.rhunk.snapenhance.features.impl.tweaks.AutoSave
import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks
import me.rhunk.snapenhance.features.impl.tweaks.DisableReplayInFF
import me.rhunk.snapenhance.features.impl.tweaks.DisableVideoLengthRestriction
import me.rhunk.snapenhance.features.impl.tweaks.SendOverride
import me.rhunk.snapenhance.features.impl.tweaks.GooglePlayServicesDialogs
import me.rhunk.snapenhance.features.impl.tweaks.LocationSpoofer
import me.rhunk.snapenhance.features.impl.tweaks.MediaQualityLevelOverride
import me.rhunk.snapenhance.features.impl.tweaks.Notifications
import me.rhunk.snapenhance.features.impl.tweaks.SendOverride
import me.rhunk.snapenhance.features.impl.tweaks.SnapchatPlus
import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime
import me.rhunk.snapenhance.features.impl.ui.PinConversations
@ -84,7 +83,6 @@ class FeatureManager(private val context: ModContext) : Manager {
register(MeoPasscodeBypass::class)
register(AppPasscode::class)
register(LocationSpoofer::class)
register(AutoUpdater::class)
register(CameraTweaks::class)
register(InfiniteStoryBoost::class)
register(AmoledDarkMode::class)