commit 3eb8c8f015380da1b9f5319ba869f082f7925970
parent 7703d3f007dc661624de1c2c6786d51132add223
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 24 Aug 2023 02:15:06 +0200

feat: streaks reminder
- add streak indicator in social
- fix dialogs
- fix container global state

Diffstat:
Mapp/src/main/AndroidManifest.xml | 3+++
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt | 23++++++++++++++++-------
Mapp/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt | 1+
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 4+++-
Aapp/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt | 21+++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt | 6+++++-
Aapp/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt | 8+++++---
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt | 25+++++++++++++++++++------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt | 32++++++++++++++++++--------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt | 17+++++++----------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt | 33++++++++++++++++++++++++++-------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt | 28++++++++++++++++------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt | 9+++++++--
Aapp/src/main/res/drawable/streak_icon.xml | 11+++++++++++
Mcore/src/main/assets/lang/en_US.json | 10++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt | 4++--
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt | 12------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt | 4+++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt | 4+++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt | 4++--
Acore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/StreaksReminderConfig.kt | 8++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt | 11++++++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 6+-----
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt | 5++++-
29 files changed, 308 insertions(+), 91 deletions(-)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> @@ -61,6 +62,8 @@ android:theme="@android:style/Theme.NoDisplay" android:excludeFromRecents="true" android:exported="true" /> + + <receiver android:name=".messaging.StreaksReminder" /> </application> </manifest> \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity import androidx.documentfile.provider.DocumentFile import coil.ImageLoader import coil.decode.VideoFrameDecoder +import coil.disk.DiskCache import coil.memory.MemoryCache import kotlinx.coroutines.Dispatchers import me.rhunk.snapenhance.bridge.BridgeService @@ -17,6 +18,7 @@ import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper import me.rhunk.snapenhance.core.config.ModConfig import me.rhunk.snapenhance.download.DownloadTaskManager import me.rhunk.snapenhance.messaging.ModDatabase +import me.rhunk.snapenhance.messaging.StreaksReminder import me.rhunk.snapenhance.ui.manager.data.InstallationSummary import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo @@ -39,6 +41,7 @@ class RemoteSideContext( val mappings = MappingsWrapper() val downloadTaskManager = DownloadTaskManager() val modDatabase = ModDatabase(this) + val streaksReminder = StreaksReminder(this) //used to load bitmoji selfies and download previews val imageLoader by lazy { @@ -48,24 +51,30 @@ class RemoteSideContext( MemoryCache.Builder(androidContext) .maxSizePercent(0.25) .build() - }.components { add(VideoFrameDecoder.Factory()) }.build() - } - - init { - reload() + } + .diskCache { + DiskCache.Builder() + .directory(androidContext.cacheDir.resolve("coil-disk-cache")) + .maxSizeBytes(1024 * 1024 * 100) // 100MB + .build() + } + .components { add(VideoFrameDecoder.Factory()) }.build() } fun reload() { runCatching { config.loadFromContext(androidContext) - translation.userLocale = config.locale - translation.loadFromContext(androidContext) + translation.apply { + userLocale = config.locale + loadFromContext(androidContext) + } mappings.apply { loadFromContext(androidContext) init(androidContext) } downloadTaskManager.init(androidContext) modDatabase.init() + streaksReminder.init() }.onFailure { Logger.error("Failed to load RemoteSideContext", it) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SharedContextHolder.kt @@ -9,6 +9,7 @@ object SharedContextHolder { fun remote(context: Context): RemoteSideContext { if (!::_remoteSideContext.isInitialized || _remoteSideContext.get() == null) { _remoteSideContext = WeakReference(RemoteSideContext(context)) + _remoteSideContext.get()?.reload() } return _remoteSideContext.get()!! diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -26,7 +26,9 @@ class BridgeService : Service() { remoteSideContext = SharedContextHolder.remote(this).apply { if (checkForRequirements()) return null } - remoteSideContext.bridgeService = this + remoteSideContext.apply { + bridgeService = this@BridgeService + } messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() } return BridgeBinder() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt @@ -0,0 +1,20 @@ +package me.rhunk.snapenhance.bridge + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import me.rhunk.snapenhance.SharedContextHolder + +class ForceStartActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (intent.getBooleanExtra("streaks_notification_action", false)) { + packageManager.getLaunchIntentForPackage("com.snapchat.android")?.apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(this) + } + SharedContextHolder.remote(this).streaksReminder.dismissAllNotifications() + } + finish() + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -140,8 +140,11 @@ class ModDatabase( ) //sync streaks if (friend.streakLength > 0) { - database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, length) VALUES (?, ?, ?)", arrayOf( + val streaks = getFriendStreaks(friend.userId!!) + + database.execSQL("INSERT OR REPLACE INTO streaks (userId, notify, expirationTimestamp, length) VALUES (?, ?, ?, ?)", arrayOf( friend.userId, + streaks?.notify ?: false, friend.streakExpirationTimestamp, friend.streakLength )) @@ -198,6 +201,7 @@ class ModDatabase( fun deleteFriend(userId: String) { executeAsync { database.execSQL("DELETE FROM friends WHERE userId = ?", arrayOf(userId)) + database.execSQL("DELETE FROM streaks WHERE userId = ?", arrayOf(userId)) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/StreaksReminder.kt @@ -0,0 +1,101 @@ +package me.rhunk.snapenhance.messaging + +import android.app.AlarmManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +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 +import me.rhunk.snapenhance.SharedContextHolder +import me.rhunk.snapenhance.bridge.ForceStartActivity +import me.rhunk.snapenhance.ui.util.ImageRequestHelper +import me.rhunk.snapenhance.util.snap.BitmojiSelfie + +class StreaksReminder( + private val remoteSideContext: RemoteSideContext? = null +): BroadcastReceiver() { + companion object { + 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( + NOTIFICATION_CHANNEL_ID, + "Streaks", + NotificationManager.IMPORTANCE_HIGH + ) + ) + } + + override fun onReceive(ctx: Context, intent: Intent) { + val remoteSideContext = this.remoteSideContext ?: SharedContextHolder.remote(ctx) + if (remoteSideContext.config.root.streaksReminder.globalState != true) return + + val notifyFriendList = remoteSideContext.modDatabase.getFriends() + .associateBy { remoteSideContext.modDatabase.getFriendStreaks(it.userId) } + .filter { (streaks, _) -> streaks != null && streaks.notify && streaks.isAboutToExpire() } + + val notificationManager = getNotificationManager(ctx) + + notifyFriendList.forEach { (streaks, friend) -> + coroutineScope.launch { + val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D) + val bitmojiImage = remoteSideContext.imageLoader.execute( + ImageRequestHelper.newBitmojiImageRequest(ctx, bitmojiUrl) + ) + + val notificationBuilder = NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID) + .setContentTitle("Streaks") + .setContentText("You will lose streaks with ${friend.displayName} in ${streaks?.hoursLeft() ?: 0} hours") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(PendingIntent.getActivity( + ctx, + 0, + Intent(ctx, ForceStartActivity::class.java).apply { + putExtra("streaks_notification_action", true) + }, + PendingIntent.FLAG_IMMUTABLE + )) + .apply { + bitmojiImage.drawable?.let { + setLargeIcon(it.toBitmap()) + setSmallIcon(R.drawable.streak_icon) + } + } + + notificationManager.notify(friend.userId.hashCode(), notificationBuilder.build().apply { + flags = NotificationCompat.FLAG_ONLY_ALERT_ONCE + }) + } + } + } + + //TODO: ask for notifications permission for a13+ + fun init() { + if (remoteSideContext == null) throw IllegalStateException("RemoteSideContext is null") + val reminderConfig = remoteSideContext.config.root.streaksReminder.also { + if (it.globalState != true) return + } + + remoteSideContext.androidContext.getSystemService(AlarmManager::class.java).setRepeating( + AlarmManager.RTC_WAKEUP, 5000, reminderConfig.interval.get().toLong() * 60 * 60 * 1000, + PendingIntent.getBroadcast(remoteSideContext.androidContext, 0, Intent(remoteSideContext.androidContext, StreaksReminder::class.java), + PendingIntent.FLAG_IMMUTABLE) + ) + + onReceive(remoteSideContext.androidContext, Intent()) + } + + fun dismissAllNotifications() = getNotificationManager(remoteSideContext!!.androidContext).cancelAll() +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt @@ -10,21 +10,23 @@ import androidx.compose.runtime.remember import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.ui.AppMaterialTheme class MainActivity : ComponentActivity() { private lateinit var sections: Map<EnumSection, Section> private lateinit var navController: NavHostController + private lateinit var managerContext: RemoteSideContext override fun onPostResume() { super.onPostResume() sections.values.forEach { it.onResumed() } } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - intent?.getStringExtra("route")?.let { route -> + intent.getStringExtra("route")?.let { route -> navController.popBackStack() navController.navigate(route) { popUpTo(navController.graph.findStartDestination().id){ @@ -38,7 +40,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME - val managerContext = SharedContextHolder.remote(this).apply { + managerContext = SharedContextHolder.remote(this).apply { activity = this@MainActivity checkForRequirements() } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -268,9 +268,9 @@ class FeaturesSection : Section() { navController.navigate(FEATURE_CONTAINER_ROUTE.replace("{name}", property.name)) } - if (container.globalState == null) return + if (!container.hasGlobalState) return - var state by remember { mutableStateOf(container.globalState!!) } + var state by remember { mutableStateOf(container.globalState ?: false) } Box( modifier = Modifier @@ -453,6 +453,22 @@ class FeaturesSection : Section() { if (showSearchBar) return var showExportDropdownMenu by remember { mutableStateOf(false) } + var showResetConfirmationDialog by remember { mutableStateOf(false) } + + if (showResetConfirmationDialog) { + Dialog(onDismissRequest = { showResetConfirmationDialog = false }) { + alertDialogs.ConfirmDialog( + title = "Reset config", + message = "Are you sure you want to reset the config?", + onConfirm = { + context.config.reset() + context.shortToast("Config successfully reset!") + }, + onDismiss = { showResetConfirmationDialog = false } + ) + } + } + val actions = remember { mapOf( "Export" to { @@ -477,10 +493,7 @@ class FeaturesSection : Section() { } } }, - "Reset" to { - context.config.reset() - context.shortToast("Config successfully reset!") - } + "Reset" to { showResetConfirmationDialog = true } ) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt @@ -14,6 +14,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -29,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -53,31 +55,34 @@ class AddFriendDialog( private val section: SocialSection, ) { @Composable - private fun ListCardEntry(name: String, exists: Boolean, stateChanged: (state: Boolean) -> Unit = { }) { - var state by remember { mutableStateOf(exists) } + private fun ListCardEntry(name: String, currentState: () -> Boolean, onState: (Boolean) -> Unit = {}) { + var currentState by remember { mutableStateOf(currentState()) } Row( modifier = Modifier .fillMaxWidth() .clickable { - state = !state - stateChanged(state) + currentState = !currentState + onState(currentState) } .padding(4.dp), - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically ) { Text( text = name, fontSize = 15.sp, modifier = Modifier .weight(1f) + .onGloballyPositioned { + currentState = currentState() + } ) - androidx.compose.material3.Checkbox( - checked = state, + Checkbox( + checked = currentState, onCheckedChange = { - state = it - stateChanged(state) + currentState = it + onState(currentState) } ) } @@ -227,13 +232,12 @@ class AddFriendDialog( items(filteredGroups.size) { val group = filteredGroups[it] - ListCardEntry( name = group.name, - exists = remember { context.modDatabase.getGroupInfo(group.conversationId) != null } + currentState = { context.modDatabase.getGroupInfo(group.conversationId) != null } ) { state -> if (state) { - context.bridgeService.triggerGroupSync(cachedGroups!![it].conversationId) + context.bridgeService.triggerGroupSync(group.conversationId) } else { context.modDatabase.deleteGroup(group.conversationId) } @@ -257,10 +261,10 @@ class AddFriendDialog( ListCardEntry( name = friend.displayName?.takeIf { name -> name.isNotBlank() } ?: friend.mutableUsername, - exists = remember { context.modDatabase.getFriendInfo(friend.userId) != null } + currentState = { context.modDatabase.getFriendInfo(friend.userId) != null } ) { state -> if (state) { - context.bridgeService.triggerFriendSync(cachedFriends!![it].userId) + context.bridgeService.triggerFriendSync(friend.userId) } else { context.modDatabase.deleteFriend(friend.userId) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -177,14 +177,11 @@ class ScopeContent( .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) -> - if (selfieId == null || bitmojiId == null) return@let null - BitmojiSelfie.getBitmojiSelfie( - selfieId, - bitmojiId, - BitmojiSelfie.BitmojiSelfieType.THREE_D - ) - } + val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie( + friend.selfieId, + friend.bitmojiId, + BitmojiSelfie.BitmojiSelfieType.THREE_D + ) BitmojiImage(context = context, url = bitmojiUrl, size = 100) Spacer(modifier = Modifier.height(16.dp)) Text( @@ -218,13 +215,13 @@ class ScopeContent( Column( modifier = Modifier.weight(1f), ) { - Text(text = "Count: ${streaks.length}", maxLines = 1) + Text(text = "Length: ${streaks.length}", maxLines = 1) Text(text = "Expires in: ${computeStreakETA(streaks.expirationTimestamp)}", maxLines = 1) } Row( verticalAlignment = Alignment.CenterVertically ) { - Text(text = "Notify Expiration", maxLines = 1) + Text(text = "Reminder", maxLines = 1, modifier = Modifier.padding(end = 10.dp)) Switch(checked = shouldNotify, onCheckedChange = { context.modDatabase.setFriendStreaksNotify(id, it) shouldNotify = it diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt @@ -18,7 +18,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.DeleteForever import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -36,6 +35,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -45,6 +46,7 @@ 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.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.core.messaging.SocialScope @@ -175,24 +177,41 @@ class SocialSection : Section() { } SocialScope.FRIEND -> { val friend = friendList[index] + val streaks = remember { context.modDatabase.getFriendStreaks(friend.userId) } + Row( modifier = Modifier .padding(10.dp) .fillMaxSize(), verticalAlignment = Alignment.CenterVertically ) { - val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) -> - if (selfieId == null || bitmojiId == null) return@let null - BitmojiSelfie.getBitmojiSelfie(selfieId, bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D) - } - BitmojiImage(context = context, url = bitmojiUrl) + BitmojiImage( + context = context, + url = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D) + ) Column( modifier = Modifier .padding(10.dp) .fillMaxWidth() + .weight(1f) ) { Text(text = friend.displayName ?: friend.mutableUsername, maxLines = 1, fontWeight = FontWeight.Bold) - Text(text = friend.userId, maxLines = 1, fontSize = 12.sp) + Text(text = friend.mutableUsername, maxLines = 1, fontSize = 12.sp, fontWeight = FontWeight.Light) + } + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (streaks != null && streaks.notify) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.streak_icon), + contentDescription = null, + modifier = Modifier.height(40.dp), + tint = if (streaks.isAboutToExpire()) + MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.primary + ) + Text(text = "${streaks.hoursLeft()}h", maxLines = 1, fontWeight = FontWeight.Bold) + } } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/AlertDialogs.kt @@ -28,9 +28,11 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.config.DataProcessors import me.rhunk.snapenhance.core.config.PropertyPair @@ -57,21 +59,22 @@ class AlertDialogs( @Composable fun ConfirmDialog( title: String, - data: String? = null, + message: String? = null, onConfirm: () -> Unit, onDismiss: () -> Unit, ) { DefaultDialogCard { Text( text = title, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(bottom = 10.dp) + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 5.dp, bottom = 10.dp) ) - if (data != null) { + if (message != null) { Text( - text = data, + text = message, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 10.dp) + modifier = Modifier.padding(bottom = 15.dp) ) } Row( @@ -91,20 +94,21 @@ class AlertDialogs( @Composable fun InfoDialog( title: String, - data: String? = null, + message: String? = null, onDismiss: () -> Unit, ) { DefaultDialogCard { Text( text = title, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(bottom = 10.dp) + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 5.dp, bottom = 10.dp) ) - if (data != null) { + if (message != null) { Text( - text = data, + text = message, style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 10.dp) + modifier = Modifier.padding(bottom = 15.dp) ) } Row( diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt @@ -36,18 +36,23 @@ fun BitmojiImage(context: RemoteSideContext, modifier: Modifier = Modifier, size ) } +fun ImageRequest.Builder.cacheKey(key: String?) = apply { + memoryCacheKey(key) + diskCacheKey(key) +} + object ImageRequestHelper { fun newBitmojiImageRequest(context: Context, url: String?) = ImageRequest.Builder(context) .data(url) .fallback(R.drawable.bitmoji_blank) .precision(Precision.INEXACT) .crossfade(true) - .memoryCacheKey(url) + .cacheKey(url) .build() fun newDownloadPreviewImageRequest(context: Context, filePath: String?) = ImageRequest.Builder(context) .data(filePath) - .memoryCacheKey(filePath) + .cacheKey(filePath) .crossfade(true) .build() } \ No newline at end of file diff --git a/app/src/main/res/drawable/streak_icon.xml b/app/src/main/res/drawable/streak_icon.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:width="48dp" + android:height="48dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="#FF000000" + android:pathData="M224.78,560q0,59.18 26.11,111.45 26.11,52.27 73.28,87.6 -4,-10.99 -6,-22.68 -2,-11.69 -2,-22.97 1.2,-30.8 13.31,-57.61 12.12,-26.8 34.4,-49.09L480,492.35l116.35,114.35q22.28,22.28 34.4,49.09 12.12,26.81 13.08,57.61 0,11.28 -2,22.97 -2,11.69 -5.76,22.68 46.7,-35.33 73.04,-87.6Q735.46,619.18 735.46,560q0,-52.09 -22.07,-102.06 -22.07,-49.97 -63.35,-91.96 -21,14.28 -43.84,22.42 -22.84,8.14 -45.21,8.14 -60.27,0 -100.63,-39.95Q420,316.65 417.37,254.61v-20q-44.36,32.26 -79.91,71.32 -35.55,39.05 -60.59,81.36 -25.04,42.3 -38.57,86.61 -13.52,44.31 -13.52,86.11ZM480,587.83l-68.31,67.56q-13.82,13.57 -20.96,29.92 -7.14,16.34 -7.14,35.64 0,39.5 28.09,66.89 28.09,27.38 68.37,27.38 40.28,0 68.33,-27.44 28.04,-27.44 28.04,-66.97 0,-19.05 -7.13,-35.4 -7.13,-16.35 -20.63,-29.93L480,587.83ZM483.59,114.26L483.59,252q0,32.48 22.45,54.44 22.45,21.97 54.94,21.97 17.21,0 32.04,-7.14 14.83,-7.14 26.35,-21.66l19.67,-24.39q76.21,43.41 120.38,119.74 44.17,76.33 44.17,164.97 0,135.53 -94.05,229.59 -94.05,94.06 -229.56,94.06 -135.51,0 -229.54,-94.04 -94.03,-94.04 -94.03,-229.55 0,-129.19 87.79,-249.61Q332,189.98 483.59,114.26Z" + tools:ignore="VectorPath" /> +</vector> diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -309,6 +309,16 @@ } } }, + "streaks_reminder": { + "name": "Streaks Reminder", + "description": "Reminds you to keep your streaks", + "properties": { + "interval": { + "name": "Interval", + "description": "The interval between each reminder (in hours)" + } + } + }, "experimental": { "name": "Experimental", "description": "Experimental features", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -35,14 +35,14 @@ class BridgeClient( fun start(callback: (Boolean) -> Unit) { this.future = CompletableFuture() + //TODO: randomize package name with(context.androidContext) { //ensure the remote process is running startActivity(Intent() - .setClassName(BuildConfig.APPLICATION_ID, ForceStartActivity::class.java.name) + .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.ForceStartActivity") .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) ) - //TODO: randomize package name val intent = Intent() .setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.BridgeService") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt @@ -1,11 +0,0 @@ -package me.rhunk.snapenhance.bridge - -import android.app.Activity -import android.os.Bundle - -class ForceStartActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - finish() - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt @@ -6,10 +6,11 @@ import kotlin.reflect.KProperty typealias ConfigParamsBuilder = ConfigParams.() -> Unit open class ConfigContainer( - var globalState: Boolean? = null + val hasGlobalState: Boolean = false ) { var parentContainerKey: PropertyKey<*>? = null val properties = mutableMapOf<PropertyKey<*>, PropertyValue<*>>() + var globalState: Boolean? = null private inline fun <T> registerProperty( key: String, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt @@ -13,6 +13,8 @@ class Global : ConfigContainer() { val disableGooglePlayDialogs = boolean("disable_google_play_dialogs") val forceMediaSourceQuality = boolean("force_media_source_quality") val betterNotifications = multiple("better_notifications", "snap", "chat", "reply_button", "download_button") - val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray()) + val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray()) { + customOptionTranslationPath = "features.options.notifications" + } val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/MessagingTweaks.kt @@ -16,7 +16,9 @@ class MessagingTweaks : ConfigContainer() { "EXTERNAL_MEDIA", "STICKER" ) - val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray()) + val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray()) { + customOptionTranslationPath = "features.options.notifications" + } val messageLogger = boolean("message_logger") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) } val galleryMediaSendOverride = boolean("gallery_media_send_override") val messagePreviewLength = integer("message_preview_length", defaultValue = 20) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt @@ -9,6 +9,7 @@ class RootConfig : ConfigContainer() { val global = container("global", Global()) { icon = "MiscellaneousServices" } val rules = container("rules", Rules()) { icon = "Rule" } val camera = container("camera", Camera()) { icon = "Camera"} + val streaksReminder = container("streaks_reminder", StreaksReminderConfig()) { icon = "Alarm" } val experimental = container("experimental", Experimental()) { icon = "Science" } val spoof = container("spoof", Spoof()) { icon = "Fingerprint" } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Spoof.kt @@ -3,13 +3,13 @@ package me.rhunk.snapenhance.core.config.impl import me.rhunk.snapenhance.core.config.ConfigContainer class Spoof : ConfigContainer() { - inner class Location : ConfigContainer(globalState = false) { + inner class Location : ConfigContainer(hasGlobalState = true) { val latitude = float("location_latitude") val longitude = float("location_longitude") } val location = container("location", Location()) - inner class Device : ConfigContainer(globalState = false) { + inner class Device : ConfigContainer(hasGlobalState = true) { val fingerprint = string("device_fingerprint") val androidId = string("device_android_id") } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/StreaksReminderConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/StreaksReminderConfig.kt @@ -0,0 +1,7 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer + +class StreaksReminderConfig : ConfigContainer(hasGlobalState = true) { + val interval = integer("interval", 2) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.core.messaging import me.rhunk.snapenhance.util.SerializableDataObject +import kotlin.time.Duration.Companion.hours enum class RuleState( @@ -45,8 +46,16 @@ data class FriendStreaks( val notify: Boolean, val expirationTimestamp: Long, val length: Int -) : SerializableDataObject() +) : SerializableDataObject() { + companion object { + //TODO: config + val EXPIRE_THRESHOLD = 12.hours + } + fun hoursLeft() = (expirationTimestamp - System.currentTimeMillis()) / 1000 / 60 / 60 + + fun isAboutToExpire() = expirationTimestamp - System.currentTimeMillis() < EXPIRE_THRESHOLD.inWholeMilliseconds +} data class MessagingGroupInfo( val conversationId: String, diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -61,11 +61,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp ): DownloadManagerClient { val generatedHash = mediaIdentifier.hashCode().toString(16).replaceFirst("-", "") - val iconUrl = friendInfo?.takeIf { - it.bitmojiAvatarId != null && it.bitmojiSelfieId != null - }?.let { - BitmojiSelfie.getBitmojiSelfie(it.bitmojiSelfieId!!, it.bitmojiAvatarId!!, BitmojiSelfie.BitmojiSelfieType.THREE_D) - } + val iconUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo?.bitmojiSelfieId, friendInfo?.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.THREE_D) val downloadLogging by context.config.downloader.logging if (downloadLogging.contains("started")) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt @@ -50,7 +50,7 @@ object ViewAppearanceHelper { } } - val snapchatFontResId = resources.getIdentifier("avenir_next_medium", "font", "com.snapchat.android") + val snapchatFontResId = resources.getIdentifier("avenir_next_medium", "font", Constants.SNAPCHAT_PACKAGE_NAME) val scalingFactor = resources.displayMetrics.densityDpi.toDouble() / 400 with(component) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -53,7 +53,7 @@ class FriendFeedInfoMenu : AbstractMenu() { profile.bitmojiSelfieId.toString(), profile.bitmojiAvatarId.toString(), BitmojiSelfie.BitmojiSelfieType.THREE_D - ) + )!! ) } } catch (e: Throwable) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt @@ -6,7 +6,10 @@ object BitmojiSelfie { THREE_D } - fun getBitmojiSelfie(selfieId: String, avatarId: String, type: BitmojiSelfieType): String { + fun getBitmojiSelfie(selfieId: String?, avatarId: String?, type: BitmojiSelfieType): String? { + if (selfieId.isNullOrEmpty() || avatarId.isNullOrEmpty()) { + return null + } return when (type) { BitmojiSelfieType.STANDARD -> "https://sdk.bitmoji.com/render/panel/$selfieId-$avatarId-v1.webp?transparent=1" BitmojiSelfieType.THREE_D -> "https://images.bitmoji.com/3d/render/$selfieId-$avatarId-v1.webp?trim=circle"