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

View File

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

View File

@ -10,7 +10,7 @@ data class SnapchatAppInfo(
) )
data class ModInfo( data class ModInfo(
val loaderPackageName: String, val loaderPackageName: String?,
val buildPackageName: String, val buildPackageName: String,
val buildVersion: String, val buildVersion: String,
val buildVersionCode: Long, val buildVersionCode: Long,
@ -22,7 +22,6 @@ data class ModInfo(
data class PlatformInfo( data class PlatformInfo(
val device: String, val device: String,
val buildFingerprint: String,
val androidVersion: String, val androidVersion: String,
val systemAbi: 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,11 +63,10 @@ import me.rhunk.snapenhance.ui.util.ImageRequestHelper
class DownloadsSection : Section() { class DownloadsSection : Section() {
private val loadedDownloads = mutableStateOf(mapOf<Int, DownloadObject>()) private val loadedDownloads = mutableStateOf(mapOf<Int, DownloadObject>())
private var currentFilter = mutableStateOf(MediaDownloadSource.NONE) private var currentFilter = mutableStateOf(MediaDownloadSource.NONE)
private val coroutineScope = CoroutineScope(Dispatchers.IO)
override fun onResumed() { override fun onResumed() {
super.onResumed() super.onResumed()
coroutineScope.launch { context.coroutineScope.launch {
loadByFilter(currentFilter.value) loadByFilter(currentFilter.value)
} }
} }
@ -129,7 +127,7 @@ class DownloadsSection : Section() {
} }
}, },
onClick = { onClick = {
coroutineScope.launch { context.coroutineScope.launch {
loadByFilter(filter) loadByFilter(filter)
showMenu = false showMenu = false
} }

View File

@ -1,5 +1,6 @@
package me.rhunk.snapenhance.ui.manager.sections.home package me.rhunk.snapenhance.ui.manager.sections.home
import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
@ -48,9 +49,11 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navigation import androidx.navigation.navigation
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.R import me.rhunk.snapenhance.R
import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary 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.setup.Requirements
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
import me.rhunk.snapenhance.ui.util.saveFile import me.rhunk.snapenhance.ui.util.saveFile
@ -64,9 +67,10 @@ class HomeSection : Section() {
const val LOGS_SECTION_ROUTE = "home_logs" const val LOGS_SECTION_ROUTE = "home_logs"
} }
private val installationSummary = mutableStateOf(null as InstallationSummary?) private var installationSummary: InstallationSummary? = null
private val userLocale = mutableStateOf(null as String?) private var userLocale: String? = null
private val homeSubSection by lazy { HomeSubSection(context) } private val homeSubSection by lazy { HomeSubSection(context) }
private var latestUpdate: Updater.LatestRelease? = null
private lateinit var activityLauncherHelper: ActivityLauncherHelper private lateinit var activityLauncherHelper: ActivityLauncherHelper
override fun init() { override fun init() {
@ -100,42 +104,16 @@ class HomeSection : Section() {
@Composable @Composable
private fun SummaryCards(installationSummary: InstallationSummary) { 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 { val summaryInfo = remember {
mapOf( mapOf(
"Build Issuer" to (installationSummary.modInfo?.buildIssuer ?: "Unknown"), "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, "Device" to installationSummary.platformInfo.device,
"Android version" to installationSummary.platformInfo.androidVersion, "Android Version" to installationSummary.platformInfo.androidVersion,
"System ABI" to installationSummary.platformInfo.systemAbi, "System ABI" to installationSummary.platformInfo.systemAbi
"Build fingerprint" to installationSummary.platformInfo.buildFingerprint
) )
} }
@ -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()) { if (!context.mappings.isMappingsLoaded()) {
context.mappings.init(context.androidContext) context.mappings.init(context.androidContext)
} }
installationSummary.value = context.installationSummary context.coroutineScope.launch {
userLocale.value = context.translation.loadedLocale.getDisplayName(Locale.getDefault()) 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 { 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(
text = "An xposed module that enhances the Snapchat experience", text = "An xposed module that enhances the Snapchat experience",
modifier = Modifier.padding(16.dp) 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 LANGUAGE = 0b00010
const val MAPPINGS = 0b00100 const val MAPPINGS = 0b00100
const val SAVE_FOLDER = 0b01000 const val SAVE_FOLDER = 0b01000
const val GRANT_PERMISSIONS = 0b10000
fun getName(requirement: Int): String { fun getName(requirement: Int): String {
return when (requirement) { return when (requirement) {
@ -12,6 +13,7 @@ object Requirements {
LANGUAGE -> "LANGUAGE" LANGUAGE -> "LANGUAGE"
MAPPINGS -> "MAPPINGS" MAPPINGS -> "MAPPINGS"
SAVE_FOLDER -> "SAVE_FOLDER" SAVE_FOLDER -> "SAVE_FOLDER"
GRANT_PERMISSIONS -> "GRANT_PERMISSIONS"
else -> "UNKNOWN" else -> "UNKNOWN"
} }
} }

View File

@ -36,6 +36,7 @@ import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.ui.AppMaterialTheme import me.rhunk.snapenhance.ui.AppMaterialTheme
import me.rhunk.snapenhance.ui.setup.screens.SetupScreen 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.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.PickLanguageScreen
import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen
@ -65,6 +66,9 @@ class SetupActivity : ComponentActivity() {
if (isFirstRun || hasRequirement(Requirements.LANGUAGE)) { if (isFirstRun || hasRequirement(Requirements.LANGUAGE)) {
add(PickLanguageScreen().apply { route = "language" }) add(PickLanguageScreen().apply { route = "language" })
} }
if (isFirstRun || hasRequirement(Requirements.GRANT_PERMISSIONS)) {
add(PermissionsScreen().apply { route = "permissions" })
}
if (isFirstRun || hasRequirement(Requirements.SAVE_FOLDER)) { if (isFirstRun || hasRequirement(Requirements.SAVE_FOLDER)) {
add(SaveFolderScreen().apply { route = "saveFolder" }) 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 androidx.activity.result.contract.ActivityResultContracts
import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.Logger
typealias ActivityLauncherCallback = (resultCode: Int, intent: Intent?) -> Unit
class ActivityLauncherHelper( class ActivityLauncherHelper(
val activity: ComponentActivity val activity: ComponentActivity,
) { ) {
private var callback: ((Intent) -> Unit)? = null private var callback: ActivityLauncherCallback? = null
private var activityResultLauncher: ActivityResultLauncher<Intent> = private var permissionResultLauncher: ActivityResultLauncher<String> =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
if (result.resultCode == ComponentActivity.RESULT_OK) { runCatching {
runCatching { callback?.let { it(if (result) ComponentActivity.RESULT_OK else ComponentActivity.RESULT_CANCELED, null) }
callback?.let { it(result.data!!) } }.onFailure {
}.onFailure { Logger.directError("Failed to process activity result", it)
Logger.directError("Failed to process activity result", it)
}
} }
callback = null 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) { if (this.callback != null) {
throw IllegalStateException("Already launching an activity") throw IllegalStateException("Already launching an activity")
} }
this.callback = callback this.callback = callback
activityResultLauncher.launch(intent) 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) { fun ActivityLauncherHelper.chooseFolder(callback: (uri: String) -> Unit) {
@ -36,8 +54,11 @@ fun ActivityLauncherHelper.chooseFolder(callback: (uri: String) -> Unit) {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) { ) { resultCode, intent ->
val uri = it.data ?: return@launch if (resultCode != ComponentActivity.RESULT_OK) {
return@launch
}
val uri = intent?.data ?: return@launch
val value = uri.toString() val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission( this.activity.contentResolver.takePersistableUriPermission(
uri, uri,
@ -55,8 +76,11 @@ fun ActivityLauncherHelper.saveFile(name: String, type: String = "*/*", callback
.putExtra(Intent.EXTRA_TITLE, name) .putExtra(Intent.EXTRA_TITLE, name)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) { ) { resultCode, intent ->
val uri = it.data ?: return@launch if (resultCode != ComponentActivity.RESULT_OK) {
return@launch
}
val uri = intent?.data ?: return@launch
val value = uri.toString() val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission( this.activity.contentResolver.takePersistableUriPermission(
uri, uri,
@ -72,8 +96,11 @@ fun ActivityLauncherHelper.openFile(type: String = "*/*", callback: (uri: String
.setType(type) .setType(type)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
) { ) { resultCode, intent ->
val uri = it.data ?: return@launch if (resultCode != ComponentActivity.RESULT_OK) {
return@launch
}
val uri = intent?.data ?: return@launch
val value = uri.toString() val value = uri.toString()
this.activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) this.activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
callback(value) callback(value)

View File

@ -101,7 +101,6 @@ class AlertDialogs(
Text( Text(
text = title, text = title,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 5.dp, bottom = 10.dp) modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
) )
if (message != null) { 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.ConfigContainer
import me.rhunk.snapenhance.core.config.FeatureNotice import me.rhunk.snapenhance.core.config.FeatureNotice
import me.rhunk.snapenhance.data.NotificationType
class Global : ConfigContainer() { class Global : ConfigContainer() {
val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.MAY_BAN) } 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 disableMetrics = boolean("disable_metrics")
val blockAds = boolean("block_ads") val blockAds = boolean("block_ads")
val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") { addNotices(FeatureNotice.MAY_BAN) } 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.ModContext
import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams 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.ConfigurationOverride
import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader 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.CameraTweaks
import me.rhunk.snapenhance.features.impl.tweaks.DisableReplayInFF import me.rhunk.snapenhance.features.impl.tweaks.DisableReplayInFF
import me.rhunk.snapenhance.features.impl.tweaks.DisableVideoLengthRestriction 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.GooglePlayServicesDialogs
import me.rhunk.snapenhance.features.impl.tweaks.LocationSpoofer import me.rhunk.snapenhance.features.impl.tweaks.LocationSpoofer
import me.rhunk.snapenhance.features.impl.tweaks.MediaQualityLevelOverride import me.rhunk.snapenhance.features.impl.tweaks.MediaQualityLevelOverride
import me.rhunk.snapenhance.features.impl.tweaks.Notifications 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.SnapchatPlus
import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime
import me.rhunk.snapenhance.features.impl.ui.PinConversations import me.rhunk.snapenhance.features.impl.ui.PinConversations
@ -84,7 +83,6 @@ class FeatureManager(private val context: ModContext) : Manager {
register(MeoPasscodeBypass::class) register(MeoPasscodeBypass::class)
register(AppPasscode::class) register(AppPasscode::class)
register(LocationSpoofer::class) register(LocationSpoofer::class)
register(AutoUpdater::class)
register(CameraTweaks::class) register(CameraTweaks::class)
register(InfiniteStoryBoost::class) register(InfiniteStoryBoost::class)
register(AmoledDarkMode::class) register(AmoledDarkMode::class)