refactor: feature load system
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
@@ -79,7 +79,7 @@ class ModContext(
|
||||
}
|
||||
|
||||
fun runOnUiThread(runnable: () -> Unit) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
if (Looper.getMainLooper().isCurrentThread) {
|
||||
runnable()
|
||||
return
|
||||
}
|
||||
@@ -95,7 +95,7 @@ class ModContext(
|
||||
runCatching {
|
||||
runnable()
|
||||
}.onFailure {
|
||||
longToast("Async task failed " + it.message)
|
||||
longToast("Async task failed: " + it.message)
|
||||
log.error("Async task failed", it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class SnapEnhance {
|
||||
appContext.mainActivity = this
|
||||
if (!appContext.mappings.isMappingsLoaded) return@hookMainActivity
|
||||
appContext.isMainActivityPaused = false
|
||||
onActivityCreate()
|
||||
onActivityCreate(this)
|
||||
appContext.actionManager.onNewIntent(intent)
|
||||
}
|
||||
|
||||
@@ -166,12 +166,12 @@ class SnapEnhance {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onActivityCreate() {
|
||||
private fun onActivityCreate(activity: Activity) {
|
||||
measureTimeMillis {
|
||||
with(appContext) {
|
||||
features.onActivityCreate()
|
||||
inAppOverlay.onActivityCreate(mainActivity!!)
|
||||
scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", mainActivity!!) }
|
||||
features.onActivityCreate(activity)
|
||||
inAppOverlay.onActivityCreate(activity)
|
||||
scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", activity) }
|
||||
actionManager.onActivityCreate()
|
||||
}
|
||||
}.also { time ->
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
abstract class BridgeFileFeature(name: String, private val bridgeFileType: InternalFileHandleType, loadParams: Int) : Feature(name, loadParams) {
|
||||
abstract class BridgeFileFeature(name: String, private val bridgeFileType: InternalFileHandleType) : Feature(name) {
|
||||
private val fileLines = mutableListOf<String>()
|
||||
private val fileWrapper by mappedLazyBridge(LazyBridgeValue({ context.fileHandlerManager.getFileHandle(FileHandleScope.INTERNAL.key, bridgeFileType.key)!! }), map = { it.toWrapper() })
|
||||
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
package me.rhunk.snapenhance.core.features
|
||||
|
||||
import android.app.Activity
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.core.ModContext
|
||||
|
||||
abstract class Feature(
|
||||
val featureKey: String,
|
||||
val loadParams: Int = FeatureLoadParams.INIT_SYNC
|
||||
val key: String
|
||||
) {
|
||||
lateinit var context: ModContext
|
||||
lateinit var registerNextActivityCallback: ((Activity) -> Unit) -> Unit
|
||||
|
||||
protected fun defer(block: () -> Unit) {
|
||||
context.coroutineScope.launch {
|
||||
runCatching {
|
||||
block()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to run onNextActivityCreate callback", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun onNextActivityCreate(defer: Boolean = false, block: (Activity) -> Unit) {
|
||||
if (defer) {
|
||||
registerNextActivityCallback {
|
||||
defer {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
registerNextActivityCallback(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* called on the main thread when the mod initialize
|
||||
*/
|
||||
open fun init() {}
|
||||
|
||||
/**
|
||||
* called on a dedicated thread when the mod initialize
|
||||
*/
|
||||
open fun asyncInit() {}
|
||||
|
||||
/**
|
||||
* called when the Snapchat Activity is created
|
||||
*/
|
||||
open fun onActivityCreate() {}
|
||||
|
||||
|
||||
/**
|
||||
* called on a dedicated thread when the Snapchat Activity is created
|
||||
*/
|
||||
open fun asyncOnActivityCreate() {}
|
||||
|
||||
protected fun findClass(name: String): Class<*> {
|
||||
return context.androidContext.classLoader.loadClass(name)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package me.rhunk.snapenhance.core.features
|
||||
|
||||
import android.app.Activity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -25,6 +26,7 @@ class FeatureManager(
|
||||
private val context: ModContext
|
||||
) {
|
||||
private val features = mutableMapOf<KClass<out Feature>, Feature>()
|
||||
private val onActivityCreateListeners = mutableListOf<(Activity) -> Unit>()
|
||||
|
||||
private fun register(vararg featureList: Feature) {
|
||||
if (context.bridgeClient.getDebugProp("disable_feature_loading") == "true") {
|
||||
@@ -37,11 +39,12 @@ class FeatureManager(
|
||||
launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
feature.context = context
|
||||
feature.registerNextActivityCallback = { block -> onActivityCreateListeners.add(block) }
|
||||
synchronized(features) {
|
||||
features[feature::class] = feature
|
||||
}
|
||||
}.onFailure {
|
||||
CoreLogger.xposedLog("Failed to register feature ${feature.featureKey}", it)
|
||||
CoreLogger.xposedLog("Failed to register feature ${feature.key}", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,65 +135,35 @@ class FeatureManager(
|
||||
DisableTelecomFramework(),
|
||||
BetterTranscript(),
|
||||
)
|
||||
initializeFeatures()
|
||||
}
|
||||
|
||||
private inline fun tryInit(feature: Feature, crossinline block: () -> Unit) {
|
||||
runCatching {
|
||||
block()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to init feature ${feature.featureKey}", it)
|
||||
context.longToast("Failed to init feature ${feature.featureKey}! Check logcat for more details.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFeatures(
|
||||
syncParam: Int,
|
||||
asyncParam: Int,
|
||||
syncAction: (Feature) -> Unit,
|
||||
asyncAction: (Feature) -> Unit
|
||||
) {
|
||||
|
||||
features.values.toList().forEach { feature ->
|
||||
if (feature.loadParams and syncParam != 0) {
|
||||
tryInit(feature) {
|
||||
syncAction(feature)
|
||||
}
|
||||
}
|
||||
if (feature.loadParams and asyncParam != 0) {
|
||||
context.coroutineScope.launch {
|
||||
tryInit(feature) {
|
||||
asyncAction(feature)
|
||||
}
|
||||
runCatching {
|
||||
measureTimeMillis {
|
||||
feature.init()
|
||||
}.also {
|
||||
context.log.verbose("Feature ${feature.key} initialized in $it ms")
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to init feature ${feature.key}", it)
|
||||
context.longToast("Failed to init feature ${feature.key}! Check logcat for more details.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeFeatures() {
|
||||
//TODO: async called when all features are initiated ?
|
||||
measureTimeMillis {
|
||||
initFeatures(
|
||||
FeatureLoadParams.INIT_SYNC,
|
||||
FeatureLoadParams.INIT_ASYNC,
|
||||
Feature::init,
|
||||
Feature::asyncInit
|
||||
)
|
||||
}.also {
|
||||
context.log.verbose("feature manager init took $it ms")
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityCreate() {
|
||||
measureTimeMillis {
|
||||
initFeatures(
|
||||
FeatureLoadParams.ACTIVITY_CREATE_SYNC,
|
||||
FeatureLoadParams.ACTIVITY_CREATE_ASYNC,
|
||||
Feature::onActivityCreate,
|
||||
Feature::asyncOnActivityCreate
|
||||
)
|
||||
}.also {
|
||||
context.log.verbose("feature manager onActivityCreate took $it ms")
|
||||
fun onActivityCreate(activity: Activity) {
|
||||
context.log.verbose("Activity created: ${activity.javaClass.simpleName}")
|
||||
onActivityCreateListeners.toList().also {
|
||||
onActivityCreateListeners.clear()
|
||||
}.forEach { activityListener ->
|
||||
measureTimeMillis {
|
||||
runCatching {
|
||||
activityListener(activity)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to run activity listener ${activityListener::class.simpleName}", it)
|
||||
}
|
||||
}.also {
|
||||
context.log.verbose("Activity listener ${activityListener::class.simpleName} executed in $it ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package me.rhunk.snapenhance.core.features
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.common.data.RuleState
|
||||
|
||||
abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType, loadParams: Int = 0) : Feature(name, loadParams) {
|
||||
abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType) : Feature(name) {
|
||||
private val listeners = mutableListOf<(String, Boolean) -> Unit>()
|
||||
|
||||
fun addStateListener(listener: (conversationId: String, newState: Boolean) -> Unit) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package me.rhunk.snapenhance.core.features.impl
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
@@ -9,7 +9,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.mapper.impl.COFObservableMapper
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class COFOverride : Feature("COF Override", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class COFOverride : Feature("COF Override") {
|
||||
var hasActionMenuV2 = false
|
||||
|
||||
override fun init() {
|
||||
|
||||
@@ -2,7 +2,7 @@ package me.rhunk.snapenhance.core.features.impl
|
||||
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.Hooker
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
@@ -22,7 +22,7 @@ data class ConfigFilter(
|
||||
val isAppExperiment: Boolean?
|
||||
)
|
||||
|
||||
class ConfigurationOverride : Feature("Configuration Override", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class ConfigurationOverride : Feature("Configuration Override") {
|
||||
override fun init() {
|
||||
context.mappings.useMapper(CompositeConfigurationProviderMapper::class) {
|
||||
fun getConfigKeyInfo(key: Any?) = runCatching {
|
||||
|
||||
@@ -10,12 +10,12 @@ import me.rhunk.snapenhance.common.data.FriendLinkType
|
||||
import me.rhunk.snapenhance.common.database.impl.FriendInfo
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
import me.rhunk.snapenhance.core.util.EvictingMap
|
||||
import java.io.InputStreamReader
|
||||
import java.util.Calendar
|
||||
|
||||
class FriendMutationObserver: Feature("FriendMutationObserver", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class FriendMutationObserver: Feature("FriendMutationObserver") {
|
||||
private val translation by lazy { context.translation.getCategory("friend_mutation_observer") }
|
||||
private val addSourceCache = EvictingMap<String, String>(500)
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import me.rhunk.snapenhance.common.data.StoryData
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
class MixerStories : Feature("MixerStories", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class MixerStories : Feature("MixerStories") {
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
override fun init() {
|
||||
val disableDiscoverSections by context.config.global.disableStorySections
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package me.rhunk.snapenhance.core.features.impl
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.mapper.impl.OperaViewerParamsMapper
|
||||
|
||||
class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride") {
|
||||
var currentPlaybackRate = 1.0F
|
||||
|
||||
data class OverrideKey(
|
||||
@@ -19,7 +18,7 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
|
||||
val value: (key: OverrideKey, value: Any?) -> Any?
|
||||
)
|
||||
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
val overrideMap = mutableMapOf<String, Override>()
|
||||
|
||||
fun overrideParam(key: String, filter: (value: Any?) -> Boolean, value: (overrideKey: OverrideKey, value: Any?) -> Any?) {
|
||||
@@ -44,37 +43,39 @@ class OperaViewerParamsOverride : Feature("OperaViewerParamsOverride", loadParam
|
||||
})
|
||||
}
|
||||
|
||||
context.mappings.useMapper(OperaViewerParamsMapper::class) {
|
||||
fun overrideParamResult(paramKey: Any, value: Any?): Any? {
|
||||
val fields = paramKey::class.java.fields
|
||||
val key = OverrideKey(
|
||||
name = fields.firstOrNull {
|
||||
it.type == String::class.java
|
||||
}?.get(paramKey)?.toString() ?: return value,
|
||||
defaultValue = fields.firstOrNull {
|
||||
it.type == Object::class.java
|
||||
}?.get(paramKey)
|
||||
)
|
||||
onNextActivityCreate {
|
||||
context.mappings.useMapper(OperaViewerParamsMapper::class) {
|
||||
fun overrideParamResult(paramKey: Any, value: Any?): Any? {
|
||||
val fields = paramKey::class.java.fields
|
||||
val key = OverrideKey(
|
||||
name = fields.firstOrNull {
|
||||
it.type == String::class.java
|
||||
}?.get(paramKey)?.toString() ?: return value,
|
||||
defaultValue = fields.firstOrNull {
|
||||
it.type == Object::class.java
|
||||
}?.get(paramKey)
|
||||
)
|
||||
|
||||
overrideMap[key.name]?.let { override ->
|
||||
if (override.filter(value)) {
|
||||
runCatching {
|
||||
return override.value(key, value)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to override param $key", it)
|
||||
overrideMap[key.name]?.let { override ->
|
||||
if (override.filter(value)) {
|
||||
runCatching {
|
||||
return override.value(key, value)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to override param $key", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
classReference.get()?.hook(getMethod.get()!!, HookStage.AFTER) { param ->
|
||||
param.setResult(overrideParamResult(param.arg(0), param.getResult()))
|
||||
}
|
||||
|
||||
classReference.get()?.hook(getMethod.get()!!, HookStage.AFTER) { param ->
|
||||
param.setResult(overrideParamResult(param.arg(0), param.getResult()))
|
||||
}
|
||||
|
||||
classReference.get()?.hook(getOrDefaultMethod.get()!!, HookStage.AFTER) { param ->
|
||||
param.setResult(overrideParamResult(param.arg(0), param.getResult()))
|
||||
classReference.get()?.hook(getOrDefaultMethod.get()!!, HookStage.AFTER) { param ->
|
||||
param.setResult(overrideParamResult(param.arg(0), param.getResult()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import me.rhunk.snapenhance.common.data.ContentType
|
||||
import me.rhunk.snapenhance.common.data.SocialScope
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
class ScopeSync : Feature("Scope Sync", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class ScopeSync : Feature("Scope Sync") {
|
||||
companion object {
|
||||
private const val DELAY_BEFORE_SYNC = 2000L
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper
|
||||
import me.rhunk.snapenhance.common.util.snap.RemoteMediaResolver
|
||||
import me.rhunk.snapenhance.core.DownloadManagerClient
|
||||
import me.rhunk.snapenhance.core.SnapEnhance
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.decoder.DecodedAttachment
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.decoder.MessageDecoder
|
||||
@@ -64,8 +63,7 @@ class SnapChapterInfo(
|
||||
)
|
||||
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD) {
|
||||
private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
|
||||
var lastSeenMapParams: ParamMap? = null
|
||||
private set
|
||||
@@ -487,50 +485,52 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
return options.any { keyFilter == null || it.contains(keyFilter, true) }
|
||||
}
|
||||
|
||||
override fun asyncOnActivityCreate() {
|
||||
context.mappings.useMapper(OperaPageViewControllerMapper::class) {
|
||||
val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param ->
|
||||
val viewState = (param.thisObject() as Any).getObjectField(viewStateField.get()!!).toString()
|
||||
if (viewState != "FULLY_DISPLAYED") {
|
||||
return@onOperaViewStateCallback
|
||||
}
|
||||
val operaLayerList = (param.thisObject() as Any).getObjectField(layerListField.get()!!) as ArrayList<*>
|
||||
val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
context.mappings.useMapper(OperaPageViewControllerMapper::class) {
|
||||
val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param ->
|
||||
val viewState = (param.thisObject() as Any).getObjectField(viewStateField.get()!!).toString()
|
||||
if (viewState != "FULLY_DISPLAYED") {
|
||||
return@onOperaViewStateCallback
|
||||
}
|
||||
val operaLayerList = (param.thisObject() as Any).getObjectField(layerListField.get()!!) as ArrayList<*>
|
||||
val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap
|
||||
|
||||
if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list"))
|
||||
return@onOperaViewStateCallback
|
||||
if (!mediaParamMap.containsKey("image_media_info") && !mediaParamMap.containsKey("video_media_info_list"))
|
||||
return@onOperaViewStateCallback
|
||||
|
||||
val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>()
|
||||
val isVideo = mediaParamMap.containsKey("video_media_info_list")
|
||||
val mediaInfoMap = mutableMapOf<SplitMediaAssetType, MediaInfo>()
|
||||
val isVideo = mediaParamMap.containsKey("video_media_info_list")
|
||||
|
||||
mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo(
|
||||
(if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!!
|
||||
)
|
||||
mediaInfoMap[SplitMediaAssetType.ORIGINAL] = MediaInfo(
|
||||
(if (isVideo) mediaParamMap["video_media_info_list"] else mediaParamMap["image_media_info"])!!
|
||||
)
|
||||
|
||||
if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) {
|
||||
mediaInfoMap[SplitMediaAssetType.OVERLAY] =
|
||||
MediaInfo(mediaParamMap["overlay_image_media_info"]!!)
|
||||
}
|
||||
lastSeenMapParams = mediaParamMap
|
||||
lastSeenMediaInfoMap = mediaInfoMap
|
||||
if (context.config.downloader.mergeOverlays.get() && mediaParamMap.containsKey("overlay_image_media_info")) {
|
||||
mediaInfoMap[SplitMediaAssetType.OVERLAY] =
|
||||
MediaInfo(mediaParamMap["overlay_image_media_info"]!!)
|
||||
}
|
||||
lastSeenMapParams = mediaParamMap
|
||||
lastSeenMediaInfoMap = mediaInfoMap
|
||||
|
||||
if (!canAutoDownload()) return@onOperaViewStateCallback
|
||||
if (!canAutoDownload()) return@onOperaViewStateCallback
|
||||
|
||||
context.executeAsync {
|
||||
runCatching {
|
||||
handleOperaMedia(mediaParamMap, mediaInfoMap, false)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to handle opera media", it)
|
||||
context.longToast(it.message)
|
||||
context.executeAsync {
|
||||
runCatching {
|
||||
handleOperaMedia(mediaParamMap, mediaInfoMap, false)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to handle opera media", it)
|
||||
context.longToast(it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName ->
|
||||
classReference.get()?.hook(
|
||||
methodName.get() ?: return@forEach,
|
||||
HookStage.AFTER, onOperaViewStateCallback
|
||||
)
|
||||
arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName ->
|
||||
classReference.get()?.hook(
|
||||
methodName.get() ?: return@forEach,
|
||||
HookStage.AFTER, onOperaViewStateCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,61 +7,62 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
||||
|
||||
class ProfilePictureDownloader : Feature("ProfilePictureDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
class ProfilePictureDownloader : Feature("ProfilePictureDownloader") {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun asyncOnActivityCreate() {
|
||||
override fun init() {
|
||||
if (!context.config.downloader.downloadProfilePictures.get()) return
|
||||
|
||||
var friendUsername: String? = null
|
||||
var backgroundUrl: String? = null
|
||||
var avatarUrl: String? = null
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view::class.java.name != "com.snap.unifiedpublicprofile.UnifiedPublicProfileView") return@subscribe
|
||||
onNextActivityCreate(defer = true) {
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view::class.java.name != "com.snap.unifiedpublicprofile.UnifiedPublicProfileView") return@subscribe
|
||||
|
||||
event.parent.addView(Button(event.parent.context).apply {
|
||||
text = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.button"]
|
||||
layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, 200, 0, 0)
|
||||
}
|
||||
setOnClickListener {
|
||||
ViewAppearanceHelper.newAlertDialogBuilder(
|
||||
this@ProfilePictureDownloader.context.mainActivity!!
|
||||
).apply {
|
||||
setTitle(this@ProfilePictureDownloader.context.translation["profile_picture_downloader.title"])
|
||||
val choices = mutableMapOf<String, String>()
|
||||
backgroundUrl?.let { choices["background_option"] = it }
|
||||
avatarUrl?.let { choices["avatar_option"] = it }
|
||||
event.parent.addView(Button(event.parent.context).apply {
|
||||
text = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.button"]
|
||||
layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, 200, 0, 0)
|
||||
}
|
||||
setOnClickListener {
|
||||
ViewAppearanceHelper.newAlertDialogBuilder(
|
||||
this@ProfilePictureDownloader.context.mainActivity!!
|
||||
).apply {
|
||||
setTitle(this@ProfilePictureDownloader.context.translation["profile_picture_downloader.title"])
|
||||
val choices = mutableMapOf<String, String>()
|
||||
backgroundUrl?.let { choices["background_option"] = it }
|
||||
avatarUrl?.let { choices["avatar_option"] = it }
|
||||
|
||||
setItems(choices.keys.map {
|
||||
this@ProfilePictureDownloader.context.translation["profile_picture_downloader.$it"]
|
||||
}.toTypedArray()) { _, which ->
|
||||
runCatching {
|
||||
this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture(
|
||||
choices.values.elementAt(which),
|
||||
friendUsername!!
|
||||
)
|
||||
}.onFailure {
|
||||
this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it)
|
||||
setItems(choices.keys.map {
|
||||
this@ProfilePictureDownloader.context.translation["profile_picture_downloader.$it"]
|
||||
}.toTypedArray()) { _, which ->
|
||||
runCatching {
|
||||
this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture(
|
||||
choices.values.elementAt(which),
|
||||
friendUsername!!
|
||||
)
|
||||
}.onFailure {
|
||||
this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it)
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
|
||||
if (!event.url.endsWith("/rpc/getPublicProfile")) return@subscribe
|
||||
event.onSuccess { buffer ->
|
||||
ProtoReader(buffer ?: return@onSuccess).followPath(1, 1, 2) {
|
||||
friendUsername = getString(2) ?: return@followPath
|
||||
followPath(4) {
|
||||
backgroundUrl = getString(2)
|
||||
avatarUrl = getString(100)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
|
||||
if (!event.url.endsWith("/rpc/getPublicProfile")) return@subscribe
|
||||
event.onSuccess { buffer ->
|
||||
ProtoReader(buffer ?: return@onSuccess).followPath(1, 1, 2) {
|
||||
friendUsername = getString(2) ?: return@followPath
|
||||
followPath(4) {
|
||||
backgroundUrl = getString(2)
|
||||
avatarUrl = getString(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper
|
||||
import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.getId
|
||||
@@ -47,7 +46,7 @@ import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
class AccountSwitcher: Feature("Account Switcher", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class AccountSwitcher: Feature("Account Switcher") {
|
||||
private var exportCallback: Pair<Int, String>? = null // requestCode -> userId
|
||||
private var importRequestCode: Int? = null
|
||||
|
||||
@@ -425,26 +424,24 @@ class AccountSwitcher: Feature("Account Switcher", loadParams = FeatureLoadParam
|
||||
mainDbShmFile?.delete()
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
if (context.config.experimental.accountSwitcher.globalState != true) return
|
||||
|
||||
val hovaHeaderSearchIcon = context.mainActivity!!.resources.getId("hova_header_search_icon")
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view.id != hovaHeaderSearchIcon) return@subscribe
|
||||
|
||||
event.view.setOnLongClickListener {
|
||||
context.mainActivity!!.vibrateLongPress()
|
||||
showManagementPopup()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun init() {
|
||||
if (context.config.experimental.accountSwitcher.globalState != true) return
|
||||
|
||||
onNextActivityCreate {
|
||||
val hovaHeaderSearchIcon = context.resources.getId("hova_header_search_icon")
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view.id != hovaHeaderSearchIcon) return@subscribe
|
||||
|
||||
event.view.setOnLongClickListener {
|
||||
context.mainActivity!!.vibrateLongPress()
|
||||
showManagementPopup()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(ActivityResultEvent::class) { event ->
|
||||
if (importRequestCode == event.requestCode) {
|
||||
importRequestCode = null
|
||||
@@ -501,7 +498,7 @@ class AccountSwitcher: Feature("Account Switcher", loadParams = FeatureLoadParam
|
||||
}
|
||||
|
||||
findClass("com.snap.identity.loginsignup.ui.LoginSignupActivity").apply {
|
||||
hook("onPostCreate", HookStage.AFTER) { param ->
|
||||
hook("onPostCreate", HookStage.AFTER) {
|
||||
context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content).addView(createComposeView(context.mainActivity!!) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -4,68 +4,69 @@ import me.rhunk.snapenhance.common.data.FriendAddSource
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
|
||||
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.mapper.impl.FriendRelationshipChangerMapper
|
||||
|
||||
class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class AddFriendSourceSpoof : Feature("AddFriendSourceSpoof") {
|
||||
var friendRelationshipChangerInstance: Any? = null
|
||||
private set
|
||||
|
||||
override fun onActivityCreate() {
|
||||
context.mappings.useMapper(FriendRelationshipChangerMapper::class) {
|
||||
classReference.get()?.hookConstructor(HookStage.AFTER) { param ->
|
||||
friendRelationshipChangerInstance = param.thisObject()
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(UnaryCallEvent::class) { event ->
|
||||
if (event.uri != "/snapchat.friending.server.FriendAction/AddFriends") return@subscribe
|
||||
val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@subscribe
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
edit {
|
||||
fun setPage(value: String) {
|
||||
remove(1)
|
||||
addString(1, value)
|
||||
}
|
||||
|
||||
editEach(2) {
|
||||
remove(3) // remove suggestion token
|
||||
fun setSource(source: FriendAddSource) {
|
||||
remove(2)
|
||||
addVarInt(2, source.id)
|
||||
}
|
||||
|
||||
when (spoofedSource) {
|
||||
"added_by_group_chat" -> {
|
||||
setPage("group_profile")
|
||||
setSource(FriendAddSource.GROUP_CHAT)
|
||||
}
|
||||
"added_by_username" -> {
|
||||
setPage("search")
|
||||
setSource(FriendAddSource.USERNAME)
|
||||
}
|
||||
"added_by_qr_code" -> {
|
||||
setPage("scan_snapcode")
|
||||
setSource(FriendAddSource.QR_CODE)
|
||||
}
|
||||
"added_by_mention" -> {
|
||||
setPage("context_card")
|
||||
setSource(FriendAddSource.MENTION)
|
||||
}
|
||||
"added_by_community" -> {
|
||||
setPage("profile")
|
||||
setSource(FriendAddSource.COMMUNITY)
|
||||
}
|
||||
"added_by_quick_add" -> {
|
||||
setPage("add_friends_button_on_top_bar_on_friends_feed")
|
||||
setSource(FriendAddSource.SUGGESTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
context.mappings.useMapper(FriendRelationshipChangerMapper::class) {
|
||||
classReference.get()?.hookConstructor(HookStage.AFTER) { param ->
|
||||
friendRelationshipChangerInstance = param.thisObject()
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
|
||||
context.event.subscribe(UnaryCallEvent::class) { event ->
|
||||
if (event.uri != "/snapchat.friending.server.FriendAction/AddFriends") return@subscribe
|
||||
val spoofedSource = context.config.experimental.addFriendSourceSpoof.getNullable() ?: return@subscribe
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
edit {
|
||||
fun setPage(value: String) {
|
||||
remove(1)
|
||||
addString(1, value)
|
||||
}
|
||||
|
||||
editEach(2) {
|
||||
remove(3) // remove suggestion token
|
||||
fun setSource(source: FriendAddSource) {
|
||||
remove(2)
|
||||
addVarInt(2, source.id)
|
||||
}
|
||||
|
||||
when (spoofedSource) {
|
||||
"added_by_group_chat" -> {
|
||||
setPage("group_profile")
|
||||
setSource(FriendAddSource.GROUP_CHAT)
|
||||
}
|
||||
"added_by_username" -> {
|
||||
setPage("search")
|
||||
setSource(FriendAddSource.USERNAME)
|
||||
}
|
||||
"added_by_qr_code" -> {
|
||||
setPage("scan_snapcode")
|
||||
setSource(FriendAddSource.QR_CODE)
|
||||
}
|
||||
"added_by_mention" -> {
|
||||
setPage("context_card")
|
||||
setSource(FriendAddSource.MENTION)
|
||||
}
|
||||
"added_by_community" -> {
|
||||
setPage("profile")
|
||||
setSource(FriendAddSource.COMMUNITY)
|
||||
}
|
||||
"added_by_quick_add" -> {
|
||||
setPage("add_friends_button_on_top_bar_on_friends_feed")
|
||||
setSource(FriendAddSource.SUGGESTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import me.rhunk.snapenhance.common.ui.AppMaterialTheme
|
||||
import me.rhunk.snapenhance.common.ui.createComposeView
|
||||
import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
||||
import me.rhunk.snapenhance.core.ui.children
|
||||
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
||||
@@ -39,7 +38,7 @@ import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import kotlin.random.Random
|
||||
|
||||
class AppLock : Feature("AppLock", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class AppLock : Feature("AppLock") {
|
||||
private var isUnlockRequested = false
|
||||
|
||||
private val rootContentView get() = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content)
|
||||
@@ -122,32 +121,34 @@ class AppLock : Feature("AppLock", loadParams = FeatureLoadParams.ACTIVITY_CREAT
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (context.config.experimental.appLock.globalState != true) return
|
||||
|
||||
Activity::class.java.apply {
|
||||
if (context.config.experimental.appLock.lockOnResume.get()) {
|
||||
hook("onResume", HookStage.BEFORE) { param ->
|
||||
if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook
|
||||
if (isUnlockRequested) return@hook
|
||||
lock(prompt = true)
|
||||
}
|
||||
hook("onPause", HookStage.BEFORE) { param ->
|
||||
if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook
|
||||
if (isUnlockRequested) return@hook
|
||||
hideRootView()
|
||||
onNextActivityCreate {
|
||||
Activity::class.java.apply {
|
||||
if (context.config.experimental.appLock.lockOnResume.get()) {
|
||||
hook("onResume", HookStage.BEFORE) { param ->
|
||||
if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook
|
||||
if (isUnlockRequested) return@hook
|
||||
lock(prompt = true)
|
||||
}
|
||||
hook("onPause", HookStage.BEFORE) { param ->
|
||||
if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook
|
||||
if (isUnlockRequested) return@hook
|
||||
hideRootView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(ActivityResultEvent::class) { event ->
|
||||
if (event.requestCode != requestCode) return@subscribe
|
||||
if (event.resultCode == Activity.RESULT_OK) {
|
||||
unlock()
|
||||
return@subscribe
|
||||
context.event.subscribe(ActivityResultEvent::class) { event ->
|
||||
if (event.requestCode != requestCode) return@subscribe
|
||||
if (event.resultCode == Activity.RESULT_OK) {
|
||||
unlock()
|
||||
return@subscribe
|
||||
}
|
||||
lock(prompt = false)
|
||||
}
|
||||
lock(prompt = false)
|
||||
lock()
|
||||
}
|
||||
lock()
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import me.rhunk.snapenhance.common.data.ContentType
|
||||
import me.rhunk.snapenhance.common.data.MessageUpdate
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@@ -19,7 +18,7 @@ import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.random.Random
|
||||
|
||||
class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS, loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class AutoOpenSnaps: MessagingRuleFeature("Auto Open Snaps", MessagingRuleType.AUTO_OPEN_SNAPS) {
|
||||
private val snapQueue = MutableSharedFlow<Pair<String, Long>>()
|
||||
private var snapQueueSize = AtomicInteger(0)
|
||||
private val openedSnaps = mutableListOf<Long>()
|
||||
|
||||
@@ -12,13 +12,12 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.features.BridgeFileFeature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.triggerRootCloseTouchEvent
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
class BestFriendPinning: BridgeFileFeature("Best Friend Pinning", InternalFileHandleType.PINNED_BEST_FRIEND, loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class BestFriendPinning: BridgeFileFeature("Best Friend Pinning", InternalFileHandleType.PINNED_BEST_FRIEND) {
|
||||
private fun updatePinnedBestFriendStatus() {
|
||||
lines().firstOrNull()?.trim()?.let {
|
||||
context.database.updatePinnedBestFriendStatus(it.substring(0, 36), "number_one_bf_for_two_months")
|
||||
|
||||
@@ -25,7 +25,6 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.RandomWalking
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
@@ -44,7 +43,7 @@ data class FriendLocation(
|
||||
val localityPieces: List<String>
|
||||
)
|
||||
|
||||
class BetterLocation : Feature("Better Location", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class BetterLocation : Feature("Better Location") {
|
||||
private val locationHistory = mutableMapOf<String, FriendLocation>()
|
||||
|
||||
private val walkRadius by lazy {
|
||||
|
||||
@@ -5,7 +5,6 @@ import me.rhunk.snapenhance.common.util.TranscriptApi
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
@@ -15,62 +14,65 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.lang.reflect.Method
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class BetterTranscript: Feature("Better Transcript", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class BetterTranscript: Feature("Better Transcript") {
|
||||
val transcriptApi by lazy { TranscriptApi() }
|
||||
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (context.config.experimental.betterTranscript.globalState != true) return
|
||||
val config = context.config.experimental.betterTranscript
|
||||
val preferredTranscriptionLang = config.preferredTranscriptionLang.getNullable()?.takeIf {
|
||||
it.isNotBlank()
|
||||
}
|
||||
|
||||
if (config.forceTranscription.get()) {
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 104) { event ->
|
||||
if (event.message.messageContent?.contentType != ContentType.NOTE) return@subscribe
|
||||
event.message.messageContent!!.content = ProtoEditor(event.message.messageContent!!.content!!).apply {
|
||||
edit(6, 1) {
|
||||
if (firstOrNull(3) == null) {
|
||||
addString(3, context.getConfigLocale())
|
||||
onNextActivityCreate {
|
||||
val config = context.config.experimental.betterTranscript
|
||||
val preferredTranscriptionLang = config.preferredTranscriptionLang.getNullable()?.takeIf {
|
||||
it.isNotBlank()
|
||||
}
|
||||
|
||||
if (config.forceTranscription.get()) {
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 104) { event ->
|
||||
if (event.message.messageContent?.contentType != ContentType.NOTE) return@subscribe
|
||||
event.message.messageContent!!.content = ProtoEditor(event.message.messageContent!!.content!!).apply {
|
||||
edit(6, 1) {
|
||||
if (firstOrNull(3) == null) {
|
||||
addString(3, context.getConfigLocale())
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
findClass("com.snapchat.client.voiceml.IVoiceMLSDK\$CppProxy").hook("asrTranscribe", HookStage.BEFORE) { param ->
|
||||
if (config.enhancedTranscript.get()) {
|
||||
val buffer = param.arg<ByteBuffer>(2).let {
|
||||
it.rewind()
|
||||
ByteArray(it.remaining()).also { it1 -> it.get(it1); it.rewind() }
|
||||
}.toByteArray()
|
||||
}
|
||||
val result = runCatching {
|
||||
transcriptApi.transcribe(
|
||||
buffer.toRequestBody(),
|
||||
lang = config.preferredTranscriptionLang.getNullable()?.takeIf {
|
||||
it.isNotBlank()
|
||||
}?.uppercase()
|
||||
)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to transcribe audio", it)
|
||||
context.shortToast("Failed to transcribe audio! Check logcat for more details.")
|
||||
}.getOrNull()
|
||||
|
||||
param.setResult(
|
||||
(param.method() as Method).returnType.dataBuilder {
|
||||
set("mError", result == null)
|
||||
set("mNlpResponses", ArrayList<Any>())
|
||||
set("mWordInfo", ArrayList<Any>())
|
||||
set("mTranscription", result)
|
||||
}
|
||||
)
|
||||
return@hook
|
||||
}
|
||||
preferredTranscriptionLang?.lowercase()?.let {
|
||||
val asrConfig = param.arg<Any>(1)
|
||||
asrConfig.getObjectFieldOrNull("mBaseConfig")?.apply {
|
||||
setObjectField("mLanguageModel", it)
|
||||
setObjectField("mUiLanguage", it)
|
||||
|
||||
findClass("com.snapchat.client.voiceml.IVoiceMLSDK\$CppProxy").hook("asrTranscribe", HookStage.BEFORE) { param ->
|
||||
if (config.enhancedTranscript.get()) {
|
||||
val buffer = param.arg<ByteBuffer>(2).let {
|
||||
it.rewind()
|
||||
ByteArray(it.remaining()).also { it1 -> it.get(it1); it.rewind() }
|
||||
}
|
||||
val result = runCatching {
|
||||
transcriptApi.transcribe(
|
||||
buffer.toRequestBody(),
|
||||
lang = config.preferredTranscriptionLang.getNullable()?.takeIf {
|
||||
it.isNotBlank()
|
||||
}?.uppercase()
|
||||
)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to transcribe audio", it)
|
||||
context.shortToast("Failed to transcribe audio! Check logcat for more details.")
|
||||
}.getOrNull()
|
||||
|
||||
param.setResult(
|
||||
(param.method() as Method).returnType.dataBuilder {
|
||||
set("mError", result == null)
|
||||
set("mNlpResponses", ArrayList<Any>())
|
||||
set("mWordInfo", ArrayList<Any>())
|
||||
set("mTranscription", result)
|
||||
}
|
||||
)
|
||||
return@hook
|
||||
}
|
||||
preferredTranscriptionLang?.lowercase()?.let {
|
||||
val asrConfig = param.arg<Any>(1)
|
||||
asrConfig.getObjectFieldOrNull("mBaseConfig")?.apply {
|
||||
setObjectField("mLanguageModel", it)
|
||||
setObjectField("mUiLanguage", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||
import me.rhunk.snapenhance.common.data.download.AudioStreamFormat
|
||||
import me.rhunk.snapenhance.common.data.download.MediaDownloadSource
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
@@ -23,7 +22,7 @@ import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class CallRecorder : Feature("Call Recorder", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class CallRecorder : Feature("Call Recorder") {
|
||||
private val httpServer = HttpServer(
|
||||
timeout = Integer.MAX_VALUE
|
||||
)
|
||||
|
||||
@@ -25,7 +25,6 @@ import me.rhunk.snapenhance.common.ui.AppMaterialTheme
|
||||
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
|
||||
import me.rhunk.snapenhance.common.ui.createComposeView
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.Hooker
|
||||
@@ -36,7 +35,7 @@ import java.lang.reflect.Proxy
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class ComposerHooks: Feature("ComposerHooks") {
|
||||
private val config by lazy { context.config.experimental.nativeHooks.composerHooks }
|
||||
private val getImportsFunctionName = Random.nextLong().absoluteValue.toString(16)
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ package me.rhunk.snapenhance.core.features.impl.experiments
|
||||
|
||||
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class ContextMenuFix: Feature("Context Menu Fix", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class ContextMenuFix: Feature("Context Menu Fix") {
|
||||
override fun init() {
|
||||
if (!context.config.experimental.contextMenuFix.get()) return
|
||||
context.event.subscribe(UnaryCallEvent::class) { event ->
|
||||
|
||||
@@ -5,13 +5,12 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.Message
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.MessageContent
|
||||
|
||||
class ConvertMessageLocally : Feature("Convert Message Edit", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class ConvertMessageLocally : Feature("Convert Message Edit") {
|
||||
private val messageCache = mutableMapOf<Long, MessageContent>()
|
||||
|
||||
private fun dispatchMessageEdit(message: Message, restore: Boolean = false) {
|
||||
@@ -64,11 +63,13 @@ class ConvertMessageLocally : Feature("Convert Message Edit", loadParams = Featu
|
||||
}.show()
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 2) {
|
||||
val clientMessageId = it.message.messageDescriptor?.messageId ?: return@subscribe
|
||||
if (!messageCache.containsKey(clientMessageId)) return@subscribe
|
||||
it.message.messageContent = messageCache[clientMessageId]
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 2) {
|
||||
val clientMessageId = it.message.messageDescriptor?.messageId ?: return@subscribe
|
||||
if (!messageCache.containsKey(clientMessageId)) return@subscribe
|
||||
it.message.messageContent = messageCache[clientMessageId]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,11 @@ import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.LSPatchUpdater
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class DeviceSpooferHook: Feature("device_spoofer", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class DeviceSpooferHook: Feature("device_spoofer") {
|
||||
private fun hookInstallerPackageName() {
|
||||
context.androidContext.packageManager::class.java.hook("getInstallerPackageName", HookStage.BEFORE) { param ->
|
||||
param.setResult("com.android.vending")
|
||||
|
||||
@@ -33,7 +33,6 @@ import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.features.impl.ui.ConversationToolbox
|
||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
||||
@@ -55,8 +54,7 @@ import kotlin.random.Random
|
||||
|
||||
class EndToEndEncryption : MessagingRuleFeature(
|
||||
"EndToEndEncryption",
|
||||
MessagingRuleType.E2E_ENCRYPTION,
|
||||
loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_SYNC or FeatureLoadParams.INIT_ASYNC
|
||||
MessagingRuleType.E2E_ENCRYPTION
|
||||
) {
|
||||
val isEnabled get() = context.config.experimental.e2eEncryption.globalState == true
|
||||
private val e2eeInterface by lazyBridge { context.bridgeClient.getE2eeInterface() }
|
||||
@@ -174,96 +172,223 @@ class EndToEndEncryption : MessagingRuleFeature(
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n", "DiscouragedApi")
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (!isEnabled) return
|
||||
|
||||
context.feature(ConversationToolbox::class).addComposable(translation["confirmation_dialogs.title"], filter = {
|
||||
context.database.getDMOtherParticipant(it) != null
|
||||
}) { dialog, conversationId ->
|
||||
val friendId = remember {
|
||||
context.database.getDMOtherParticipant(conversationId)
|
||||
} ?: return@addComposable
|
||||
val fingerprint = remember {
|
||||
runCatching {
|
||||
e2eeInterface.getSecretFingerprint(friendId)
|
||||
}.getOrNull()
|
||||
}
|
||||
if (fingerprint != null) {
|
||||
Text(translation.format("toolbox.shared_key_fingerprint", "fingerprint" to fingerprint))
|
||||
} else {
|
||||
Text(translation["toolbox.no_shared_key"])
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Button(onClick = {
|
||||
dialog.dismiss()
|
||||
warnKeyOverwrite(friendId) {
|
||||
askForKeys(conversationId)
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
callbacks.getClass("ConversationManagerDelegate")?.hook("onSendComplete", HookStage.BEFORE) { param ->
|
||||
val sendMessageResult = param.arg<Any>(0)
|
||||
val messageDestinations = MessageDestinations(sendMessageResult.getObjectField("mCompletedDestinations") ?: return@hook)
|
||||
if (messageDestinations.mPhoneNumbers?.isNotEmpty() == true || messageDestinations.stories?.isNotEmpty() == true) return@hook
|
||||
|
||||
val completedConversationDestinations = sendMessageResult.getObjectField("mCompletedConversationDestinations") as? ArrayList<*> ?: return@hook
|
||||
val messageIds = completedConversationDestinations.filter { getState(SnapUUID(it.getObjectField("mConversationId")).toString()) }.mapNotNull {
|
||||
it.getObjectFieldOrNull("mMessageId") as? Long
|
||||
}
|
||||
}) {
|
||||
Text(translation["toolbox.initiate_exchange_button"])
|
||||
|
||||
encryptedMessages.addAll(messageIds)
|
||||
}
|
||||
}
|
||||
|
||||
val encryptedMessageIndicator by context.config.experimental.e2eEncryption.encryptedMessageIndicator
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 0) { event ->
|
||||
val message = event.message
|
||||
val conversationId = message.messageDescriptor!!.conversationId.toString()
|
||||
val isMessageCommitted = message.messageState == MessageState.COMMITTED
|
||||
messageHook(
|
||||
conversationId = conversationId,
|
||||
messageId = message.messageDescriptor!!.messageId!!,
|
||||
senderId = message.senderId.toString(),
|
||||
messageContent = message.messageContent!!,
|
||||
committed = isMessageCommitted
|
||||
)
|
||||
|
||||
val specialCard = Random.nextLong().toString(16)
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { conversationId, messageId ->
|
||||
val viewGroup = event.view.parent as? ViewGroup ?: return@subscribe
|
||||
|
||||
viewGroup.findViewWithTag<View>(specialCard)?.also {
|
||||
viewGroup.removeView(it)
|
||||
message.messageContent!!.instanceNonNull()
|
||||
.getObjectField("mQuotedMessage")
|
||||
?.getObjectField("mContent")
|
||||
?.also { quotedMessage ->
|
||||
messageHook(
|
||||
conversationId = conversationId,
|
||||
messageId = quotedMessage.getObjectField("mMessageId")?.toString()?.toLong() ?: return@also,
|
||||
senderId = SnapUUID(quotedMessage.getObjectField("mSenderId")).toString(),
|
||||
messageContent = MessageContent(quotedMessage),
|
||||
committed = isMessageCommitted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (encryptedMessageIndicator) {
|
||||
viewGroup.removeForegroundDrawable("encryptedMessage")
|
||||
onNextActivityCreate(defer = true) {
|
||||
context.feature(ConversationToolbox::class).addComposable(translation["confirmation_dialogs.title"], filter = {
|
||||
context.database.getDMOtherParticipant(it) != null
|
||||
}) { dialog, conversationId ->
|
||||
val friendId = remember {
|
||||
context.database.getDMOtherParticipant(conversationId)
|
||||
} ?: return@addComposable
|
||||
val fingerprint = remember {
|
||||
runCatching {
|
||||
e2eeInterface.getSecretFingerprint(friendId)
|
||||
}.getOrNull()
|
||||
}
|
||||
if (fingerprint != null) {
|
||||
Text(translation.format("toolbox.shared_key_fingerprint", "fingerprint" to fingerprint))
|
||||
} else {
|
||||
Text(translation["toolbox.no_shared_key"])
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Button(onClick = {
|
||||
dialog.dismiss()
|
||||
warnKeyOverwrite(friendId) {
|
||||
askForKeys(conversationId)
|
||||
}
|
||||
}) {
|
||||
Text(translation["toolbox.initiate_exchange_button"])
|
||||
}
|
||||
}
|
||||
|
||||
if (encryptedMessages.contains(messageId.toLong())) {
|
||||
viewGroup.addForegroundDrawable("encryptedMessage", ShapeDrawable(object: Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
paint.textSize = 20f
|
||||
canvas.drawText("\uD83D\uDD12", 0f, canvas.height / 2f, paint)
|
||||
val encryptedMessageIndicator by context.config.experimental.e2eEncryption.encryptedMessageIndicator
|
||||
|
||||
val specialCard = Random.nextLong().toString(16)
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { conversationId, messageId ->
|
||||
val viewGroup = event.view.parent as? ViewGroup ?: return@subscribe
|
||||
|
||||
viewGroup.findViewWithTag<View>(specialCard)?.also {
|
||||
viewGroup.removeView(it)
|
||||
}
|
||||
|
||||
if (encryptedMessageIndicator) {
|
||||
viewGroup.removeForegroundDrawable("encryptedMessage")
|
||||
|
||||
if (encryptedMessages.contains(messageId.toLong())) {
|
||||
viewGroup.addForegroundDrawable("encryptedMessage", ShapeDrawable(object: Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
paint.textSize = 20f
|
||||
canvas.drawText("\uD83D\uDD12", 0f, canvas.height / 2f, paint)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
val secret = secretResponses[messageId.toLong()]
|
||||
val publicKey = pkRequests[messageId.toLong()]
|
||||
|
||||
if (publicKey != null || secret != null) {
|
||||
viewGroup.addView(createComposeView(context.mainActivity!!) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
onClick = {
|
||||
if (publicKey != null) {
|
||||
handlePublicKeyRequest(conversationId, publicKey)
|
||||
}
|
||||
if (secret != null) {
|
||||
handleSecretResponse(conversationId, secret)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (publicKey != null) {
|
||||
Text(translation["accept_public_key_button"])
|
||||
}
|
||||
if (secret != null) {
|
||||
Text(translation["accept_secret_button"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}.apply {
|
||||
tag = specialCard
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val secret = secretResponses[messageId.toLong()]
|
||||
val publicKey = pkRequests[messageId.toLong()]
|
||||
defer {
|
||||
val forceMessageEncryption by context.config.experimental.e2eEncryption.forceMessageEncryption
|
||||
|
||||
if (publicKey != null || secret != null) {
|
||||
viewGroup.addView(createComposeView(context.mainActivity!!) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
onClick = {
|
||||
if (publicKey != null) {
|
||||
handlePublicKeyRequest(conversationId, publicKey)
|
||||
}
|
||||
if (secret != null) {
|
||||
handleSecretResponse(conversationId, secret)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (publicKey != null) {
|
||||
Text(translation["accept_public_key_button"])
|
||||
}
|
||||
if (secret != null) {
|
||||
Text(translation["accept_secret_button"])
|
||||
}
|
||||
}
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
callbacks.getClass("UploadDelegate")?.hook("uploadMedia", HookStage.BEFORE) { param ->
|
||||
val messageDestinations = MessageDestinations(param.arg(1))
|
||||
val uploadCallback = param.arg<Any>(2)
|
||||
val e2eeConversations = messageDestinations.getEndToEndConversations()
|
||||
if (e2eeConversations.isEmpty()) return@hook
|
||||
|
||||
if (messageDestinations.conversations!!.size != e2eeConversations.size || messageDestinations.stories?.isNotEmpty() == true) {
|
||||
context.log.debug("skipping encryption")
|
||||
return@hook
|
||||
}
|
||||
|
||||
Hooker.hookObjectMethod(uploadCallback::class.java, uploadCallback, "onUploadFinished", HookStage.BEFORE) { methodParam ->
|
||||
val messageContent = MessageContent(methodParam.arg(1))
|
||||
runCatching {
|
||||
messageContent.content = ProtoWriter().apply {
|
||||
writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!)
|
||||
}.toByteArray()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to encrypt message", it)
|
||||
context.longToast(translation["encryption_failed_toast"])
|
||||
}
|
||||
}.apply {
|
||||
tag = specialCard
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trick to disable fidelius encryption
|
||||
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
|
||||
val messageContent = event.messageContent
|
||||
val destinations = event.destinations
|
||||
|
||||
val e2eeConversations = destinations.getEndToEndConversations().takeIf { it.isNotEmpty() } ?: return@subscribe
|
||||
|
||||
if (e2eeConversations.size != destinations.conversations!!.size || destinations.stories?.isNotEmpty() == true) {
|
||||
if (!forceMessageEncryption) return@subscribe
|
||||
context.longToast(translation["unencrypted_conversation_send_failure_toast"])
|
||||
event.canceled = true
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (!NativeLib.initialized) {
|
||||
context.longToast(translation["native_hooks_send_failure_toast"])
|
||||
event.canceled = true
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
event.addInvokeLater {
|
||||
if (event.messageContent.localMediaReferences?.isEmpty() == true) {
|
||||
runCatching {
|
||||
event.messageContent.content = ProtoWriter().apply {
|
||||
writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!)
|
||||
}.toByteArray()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to encrypt message", it)
|
||||
context.longToast(translation["encryption_failed_toast"])
|
||||
}
|
||||
}
|
||||
|
||||
if (event.messageContent.contentType == ContentType.SNAP) {
|
||||
event.messageContent.contentType = ContentType.EXTERNAL_MEDIA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(NativeUnaryCallEvent::class) { event ->
|
||||
if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe
|
||||
val protoReader = ProtoReader(event.buffer)
|
||||
val messageReader = protoReader.followPath(4) ?: return@subscribe
|
||||
|
||||
if (messageReader.getVarInt(4, 2, 1, 5) == 1L) {
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
edit(4) {
|
||||
remove(2)
|
||||
addVarInt(2, ContentType.SNAP.id)
|
||||
context.log.verbose("fixed snap content type")
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,135 +565,5 @@ class EndToEndEncryption : MessagingRuleFeature(
|
||||
return conversations!!.filter { getState(it.toString()) && getE2EParticipants(it.toString()).isNotEmpty() }.map { it.toString() }
|
||||
}
|
||||
|
||||
override fun asyncInit() {
|
||||
if (!isEnabled) return
|
||||
val forceMessageEncryption by context.config.experimental.e2eEncryption.forceMessageEncryption
|
||||
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
callbacks.getClass("UploadDelegate")?.hook("uploadMedia", HookStage.BEFORE) { param ->
|
||||
val messageDestinations = MessageDestinations(param.arg(1))
|
||||
val uploadCallback = param.arg<Any>(2)
|
||||
val e2eeConversations = messageDestinations.getEndToEndConversations()
|
||||
if (e2eeConversations.isEmpty()) return@hook
|
||||
|
||||
if (messageDestinations.conversations!!.size != e2eeConversations.size || messageDestinations.stories?.isNotEmpty() == true) {
|
||||
context.log.debug("skipping encryption")
|
||||
return@hook
|
||||
}
|
||||
|
||||
Hooker.hookObjectMethod(uploadCallback::class.java, uploadCallback, "onUploadFinished", HookStage.BEFORE) { methodParam ->
|
||||
val messageContent = MessageContent(methodParam.arg(1))
|
||||
runCatching {
|
||||
messageContent.content = ProtoWriter().apply {
|
||||
writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!)
|
||||
}.toByteArray()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to encrypt message", it)
|
||||
context.longToast(translation["encryption_failed_toast"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trick to disable fidelius encryption
|
||||
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
|
||||
val messageContent = event.messageContent
|
||||
val destinations = event.destinations
|
||||
|
||||
val e2eeConversations = destinations.getEndToEndConversations().takeIf { it.isNotEmpty() } ?: return@subscribe
|
||||
|
||||
if (e2eeConversations.size != destinations.conversations!!.size || destinations.stories?.isNotEmpty() == true) {
|
||||
if (!forceMessageEncryption) return@subscribe
|
||||
context.longToast(translation["unencrypted_conversation_send_failure_toast"])
|
||||
event.canceled = true
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (!NativeLib.initialized) {
|
||||
context.longToast(translation["native_hooks_send_failure_toast"])
|
||||
event.canceled = true
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
event.addInvokeLater {
|
||||
if (event.messageContent.localMediaReferences?.isEmpty() == true) {
|
||||
runCatching {
|
||||
event.messageContent.content = ProtoWriter().apply {
|
||||
writeEncryptedMessage(e2eeConversations.map { getE2EParticipants(it) }.flatten().distinct(), messageContent.content!!)
|
||||
}.toByteArray()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to encrypt message", it)
|
||||
context.longToast(translation["encryption_failed_toast"])
|
||||
}
|
||||
}
|
||||
|
||||
if (event.messageContent.contentType == ContentType.SNAP) {
|
||||
event.messageContent.contentType = ContentType.EXTERNAL_MEDIA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(NativeUnaryCallEvent::class) { event ->
|
||||
if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe
|
||||
val protoReader = ProtoReader(event.buffer)
|
||||
val messageReader = protoReader.followPath(4) ?: return@subscribe
|
||||
|
||||
if (messageReader.getVarInt(4, 2, 1, 5) == 1L) {
|
||||
event.buffer = ProtoEditor(event.buffer).apply {
|
||||
edit(4) {
|
||||
remove(2)
|
||||
addVarInt(2, ContentType.SNAP.id)
|
||||
context.log.verbose("fixed snap content type")
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
if (!isEnabled) return
|
||||
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
callbacks.getClass("ConversationManagerDelegate")?.hook("onSendComplete", HookStage.BEFORE) { param ->
|
||||
val sendMessageResult = param.arg<Any>(0)
|
||||
val messageDestinations = MessageDestinations(sendMessageResult.getObjectField("mCompletedDestinations") ?: return@hook)
|
||||
if (messageDestinations.mPhoneNumbers?.isNotEmpty() == true || messageDestinations.stories?.isNotEmpty() == true) return@hook
|
||||
|
||||
val completedConversationDestinations = sendMessageResult.getObjectField("mCompletedConversationDestinations") as? ArrayList<*> ?: return@hook
|
||||
val messageIds = completedConversationDestinations.filter { getState(SnapUUID(it.getObjectField("mConversationId")).toString()) }.mapNotNull {
|
||||
it.getObjectFieldOrNull("mMessageId") as? Long
|
||||
}
|
||||
|
||||
encryptedMessages.addAll(messageIds)
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 0) { event ->
|
||||
val message = event.message
|
||||
val conversationId = message.messageDescriptor!!.conversationId.toString()
|
||||
val isMessageCommitted = message.messageState == MessageState.COMMITTED
|
||||
messageHook(
|
||||
conversationId = conversationId,
|
||||
messageId = message.messageDescriptor!!.messageId!!,
|
||||
senderId = message.senderId.toString(),
|
||||
messageContent = message.messageContent!!,
|
||||
committed = isMessageCommitted
|
||||
)
|
||||
|
||||
message.messageContent!!.instanceNonNull()
|
||||
.getObjectField("mQuotedMessage")
|
||||
?.getObjectField("mContent")
|
||||
?.also { quotedMessage ->
|
||||
messageHook(
|
||||
conversationId = conversationId,
|
||||
messageId = quotedMessage.getObjectField("mMessageId")?.toString()?.toLong() ?: return@also,
|
||||
senderId = SnapUUID(quotedMessage.getObjectField("mSenderId")).toString(),
|
||||
messageContent = MessageContent(quotedMessage),
|
||||
committed = isMessageCommitted
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRuleState() = RuleState.WHITELIST
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.experiments
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.mapper.impl.StoryBoostStateMapper
|
||||
|
||||
class InfiniteStoryBoost : Feature("InfiniteStoryBoost", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
override fun asyncOnActivityCreate() {
|
||||
class InfiniteStoryBoost : Feature("InfiniteStoryBoost") {
|
||||
override fun init() {
|
||||
if (!context.config.experimental.infiniteStoryBoost.get()) return
|
||||
|
||||
context.mappings.useMapper(StoryBoostStateMapper::class) {
|
||||
classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
|
||||
val startTimeMillis = param.arg<Long>(1)
|
||||
//reset timestamp if it's more than 24 hours
|
||||
if (System.currentTimeMillis() - startTimeMillis > 86400000) {
|
||||
param.setArg(1, 0)
|
||||
param.setArg(2, 0)
|
||||
onNextActivityCreate(defer = true) {
|
||||
context.mappings.useMapper(StoryBoostStateMapper::class) {
|
||||
classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
|
||||
val startTimeMillis = param.arg<Long>(1)
|
||||
//reset timestamp if it's more than 24 hours
|
||||
if (System.currentTimeMillis() - startTimeMillis > 86400000) {
|
||||
param.setArg(1, 0)
|
||||
param.setArg(2, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import me.rhunk.snapenhance.common.util.ktx.getTypeArguments
|
||||
import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
@@ -41,198 +40,200 @@ import java.io.InputStream
|
||||
import java.lang.reflect.Method
|
||||
import kotlin.random.Random
|
||||
|
||||
class MediaFilePicker : Feature("Media File Picker", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class MediaFilePicker : Feature("Media File Picker") {
|
||||
var lastMediaDuration: Long? = null
|
||||
private set
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (!context.config.experimental.mediaFilePicker.get()) return
|
||||
|
||||
lateinit var chatMediaDrawerActionHandler: Any
|
||||
lateinit var sendItemsMethod: Method
|
||||
onNextActivityCreate(defer = true) {
|
||||
lateinit var chatMediaDrawerActionHandler: Any
|
||||
lateinit var sendItemsMethod: Method
|
||||
|
||||
findClass("com.snap.composer.memories.ChatMediaDrawer").genericSuperclass?.getTypeArguments()?.getOrNull(1)?.apply {
|
||||
methods.first {
|
||||
it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler")
|
||||
}.also { method ->
|
||||
sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" }
|
||||
}.hook(HookStage.AFTER) {
|
||||
chatMediaDrawerActionHandler = it.arg(0)
|
||||
findClass("com.snap.composer.memories.ChatMediaDrawer").genericSuperclass?.getTypeArguments()?.getOrNull(1)?.apply {
|
||||
methods.first {
|
||||
it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler")
|
||||
}.also { method ->
|
||||
sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" }
|
||||
}.hook(HookStage.AFTER) {
|
||||
chatMediaDrawerActionHandler = it.arg(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var requestCode: Int? = null
|
||||
var firstVideoId: Long? = null
|
||||
var mediaInputStream: InputStream? = null
|
||||
var requestCode: Int? = null
|
||||
var firstVideoId: Long? = null
|
||||
var mediaInputStream: InputStream? = null
|
||||
|
||||
ContentResolver::class.java.apply {
|
||||
hook("query", HookStage.AFTER) { param ->
|
||||
val uri = param.arg<Uri>(0)
|
||||
if (!uri.toString().endsWith(firstVideoId.toString())) return@hook
|
||||
ContentResolver::class.java.apply {
|
||||
hook("query", HookStage.AFTER) { param ->
|
||||
val uri = param.arg<Uri>(0)
|
||||
if (!uri.toString().endsWith(firstVideoId.toString())) return@hook
|
||||
|
||||
param.setResult(object: CursorWrapper(param.getResult() as Cursor) {
|
||||
override fun getLong(columnIndex: Int): Long {
|
||||
if (getColumnName(columnIndex) == "duration") {
|
||||
return lastMediaDuration ?: -1
|
||||
param.setResult(object: CursorWrapper(param.getResult() as Cursor) {
|
||||
override fun getLong(columnIndex: Int): Long {
|
||||
if (getColumnName(columnIndex) == "duration") {
|
||||
return lastMediaDuration ?: -1
|
||||
}
|
||||
return super.getLong(columnIndex)
|
||||
}
|
||||
return super.getLong(columnIndex)
|
||||
})
|
||||
}
|
||||
hook("openInputStream", HookStage.BEFORE) { param ->
|
||||
val uri = param.arg<Uri>(0)
|
||||
if (uri.toString().endsWith(firstVideoId.toString())) {
|
||||
param.setResult(mediaInputStream)
|
||||
mediaInputStream = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(ActivityResultEvent::class) { event ->
|
||||
if (event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe
|
||||
requestCode = null
|
||||
|
||||
firstVideoId = context.androidContext.contentResolver.query(
|
||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
|
||||
arrayOf(MediaStore.Video.Media._ID),
|
||||
null,
|
||||
null,
|
||||
"${MediaStore.Video.Media.DATE_TAKEN} DESC"
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getLongOrNull("_id")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (firstVideoId == null) {
|
||||
context.inAppOverlay.showStatusToast(
|
||||
Icons.Default.Upload,
|
||||
"Must have a video in gallery to upload."
|
||||
)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
fun sendMedia() {
|
||||
sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf<Any>(), listOf(
|
||||
sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder {
|
||||
from("_item") {
|
||||
set("_cameraRollSource", "Snapchat")
|
||||
set("_contentUri", "")
|
||||
set("_durationMs", 0.0)
|
||||
set("_disabled", false)
|
||||
set("_imageRotation", 0.0)
|
||||
set("_width", 1080.0)
|
||||
set("_height", 1920.0)
|
||||
set("_timestampMs", System.currentTimeMillis().toDouble())
|
||||
from("_itemId") {
|
||||
set("_itemId", firstVideoId.toString())
|
||||
set("_type", "VIDEO")
|
||||
}
|
||||
}
|
||||
set("_order", 0.0)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fun startConversation(audioOnly: Boolean) {
|
||||
context.coroutineScope.launch {
|
||||
lastMediaDuration = MediaPlayer().run {
|
||||
setDataSource(context.androidContext, event.intent.data!!)
|
||||
prepare()
|
||||
duration.toLong().also {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.Crop, "Converting media...", durationMs = 3000)
|
||||
val pfd = context.bridgeClient.convertMedia(
|
||||
context.androidContext.contentResolver.openFileDescriptor(event.intent.data!!, "r")!!,
|
||||
"m4a",
|
||||
"m4a",
|
||||
"aac",
|
||||
if (!audioOnly) "libx264" else null
|
||||
)
|
||||
|
||||
if (pfd == null) {
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to convert media.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.CheckCircleOutline, "Media converted successfully.")
|
||||
|
||||
runCatching {
|
||||
mediaInputStream = ParcelFileDescriptor.AutoCloseInputStream(pfd)
|
||||
sendMedia()
|
||||
}.onFailure {
|
||||
mediaInputStream = null
|
||||
context.log.error(it)
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isAudio = context.androidContext.contentResolver.getType(event.intent.data!!)!!.startsWith("audio/")
|
||||
|
||||
if (isAudio || context.config.messaging.galleryMediaSendOverride.getNullable() == null) {
|
||||
startConversation(isAudio)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!)
|
||||
.setTitle("Convert video file")
|
||||
.setItems(arrayOf("Send as video/audio", "Send as audio only")) { _, which ->
|
||||
startConversation(which == 1)
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }.show()
|
||||
}
|
||||
|
||||
val buttonTag = Random.nextInt(0, 65535)
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.parent.id != context.resources.getId("chat_drawer_container") || !event.view::class.java.name.endsWith("ChatMediaDrawer")) return@subscribe
|
||||
|
||||
event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
if (event.parent.findViewWithTag<View>(buttonTag)?.run {
|
||||
visibility = View.VISIBLE
|
||||
bringToFront()
|
||||
} != null) return
|
||||
event.parent.addView(
|
||||
createComposeView(context.mainActivity!!) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
FilledIconButton(onClick = {
|
||||
requestCode = Random.nextInt(0, 65535)
|
||||
this@MediaFilePicker.context.mainActivity!!.startActivityForResult(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "video/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "audio/*"))
|
||||
},
|
||||
requestCode!!
|
||||
)
|
||||
}) {
|
||||
Icon(Icons.Default.Upload, "Upload media")
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
tag = buttonTag
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
override fun onViewDetachedFromWindow(v: View) {
|
||||
event.parent.findViewWithTag<View>(buttonTag)?.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
hook("openInputStream", HookStage.BEFORE) { param ->
|
||||
val uri = param.arg<Uri>(0)
|
||||
if (uri.toString().endsWith(firstVideoId.toString())) {
|
||||
param.setResult(mediaInputStream)
|
||||
mediaInputStream = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(ActivityResultEvent::class) { event ->
|
||||
if (event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe
|
||||
requestCode = null
|
||||
|
||||
firstVideoId = context.androidContext.contentResolver.query(
|
||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
|
||||
arrayOf(MediaStore.Video.Media._ID),
|
||||
null,
|
||||
null,
|
||||
"${MediaStore.Video.Media.DATE_TAKEN} DESC"
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getLongOrNull("_id")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (firstVideoId == null) {
|
||||
context.inAppOverlay.showStatusToast(
|
||||
Icons.Default.Upload,
|
||||
"Must have a video in gallery to upload."
|
||||
)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
fun sendMedia() {
|
||||
sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf<Any>(), listOf(
|
||||
sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder {
|
||||
from("_item") {
|
||||
set("_cameraRollSource", "Snapchat")
|
||||
set("_contentUri", "")
|
||||
set("_durationMs", 0.0)
|
||||
set("_disabled", false)
|
||||
set("_imageRotation", 0.0)
|
||||
set("_width", 1080.0)
|
||||
set("_height", 1920.0)
|
||||
set("_timestampMs", System.currentTimeMillis().toDouble())
|
||||
from("_itemId") {
|
||||
set("_itemId", firstVideoId.toString())
|
||||
set("_type", "VIDEO")
|
||||
}
|
||||
}
|
||||
set("_order", 0.0)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fun startConversation(audioOnly: Boolean) {
|
||||
context.coroutineScope.launch {
|
||||
lastMediaDuration = MediaPlayer().run {
|
||||
setDataSource(context.androidContext, event.intent.data!!)
|
||||
prepare()
|
||||
duration.toLong().also {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.Crop, "Converting media...", durationMs = 3000)
|
||||
val pfd = context.bridgeClient.convertMedia(
|
||||
context.androidContext.contentResolver.openFileDescriptor(event.intent.data!!, "r")!!,
|
||||
"m4a",
|
||||
"m4a",
|
||||
"aac",
|
||||
if (!audioOnly) "libx264" else null
|
||||
)
|
||||
|
||||
if (pfd == null) {
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to convert media.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.CheckCircleOutline, "Media converted successfully.")
|
||||
|
||||
runCatching {
|
||||
mediaInputStream = ParcelFileDescriptor.AutoCloseInputStream(pfd)
|
||||
sendMedia()
|
||||
}.onFailure {
|
||||
mediaInputStream = null
|
||||
context.log.error(it)
|
||||
context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isAudio = context.androidContext.contentResolver.getType(event.intent.data!!)!!.startsWith("audio/")
|
||||
|
||||
if (isAudio || context.config.messaging.galleryMediaSendOverride.getNullable() == null) {
|
||||
startConversation(isAudio)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!)
|
||||
.setTitle("Convert video file")
|
||||
.setItems(arrayOf("Send as video/audio", "Send as audio only")) { _, which ->
|
||||
startConversation(which == 1)
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }.show()
|
||||
}
|
||||
|
||||
val buttonTag = Random.nextInt(0, 65535)
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.parent.id != context.resources.getId("chat_drawer_container") || !event.view::class.java.name.endsWith("ChatMediaDrawer")) return@subscribe
|
||||
|
||||
event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
if (event.parent.findViewWithTag<View>(buttonTag)?.run {
|
||||
visibility = View.VISIBLE
|
||||
bringToFront()
|
||||
} != null) return
|
||||
event.parent.addView(
|
||||
createComposeView(context.mainActivity!!) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
FilledIconButton(onClick = {
|
||||
requestCode = Random.nextInt(0, 65535)
|
||||
this@MediaFilePicker.context.mainActivity!!.startActivityForResult(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "video/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "audio/*"))
|
||||
},
|
||||
requestCode!!
|
||||
)
|
||||
}) {
|
||||
Icon(Icons.Default.Upload, "Upload media")
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
tag = buttonTag
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
override fun onViewDetachedFromWindow(v: View) {
|
||||
event.parent.findViewWithTag<View>(buttonTag)?.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.experiments
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.mapper.impl.BCryptClassMapper
|
||||
|
||||
class MeoPasscodeBypass : Feature("Meo Passcode Bypass", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
override fun asyncOnActivityCreate() {
|
||||
class MeoPasscodeBypass : Feature("Meo Passcode Bypass") {
|
||||
override fun init() {
|
||||
if (!context.config.experimental.meoPasscodeBypass.get()) return
|
||||
|
||||
context.mappings.useMapper(BCryptClassMapper::class) {
|
||||
classReference.get()?.hook(
|
||||
hashMethod.get()!!,
|
||||
HookStage.BEFORE,
|
||||
) { param ->
|
||||
//set the hash to the result of the method
|
||||
param.setResult(param.arg(1))
|
||||
onNextActivityCreate(defer = true) {
|
||||
context.mappings.useMapper(BCryptClassMapper::class) {
|
||||
classReference.get()?.hook(
|
||||
hashMethod.get()!!,
|
||||
HookStage.BEFORE,
|
||||
) { param ->
|
||||
//set the hash to the result of the method
|
||||
param.setResult(param.arg(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.experiments
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.mapper.impl.ScoreUpdateMapper
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class NoFriendScoreDelay : Feature("NoFriendScoreDelay", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class NoFriendScoreDelay : Feature("NoFriendScoreDelay") {
|
||||
override fun init() {
|
||||
if (!context.config.experimental.noFriendScoreDelay.get()) return
|
||||
|
||||
context.mappings.useMapper(ScoreUpdateMapper::class) {
|
||||
classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
|
||||
param.args().indexOfFirst {
|
||||
val longValue = it.toString().toLongOrNull() ?: return@indexOfFirst false
|
||||
longValue > 30.minutes.inWholeMilliseconds && longValue < 10.days.inWholeMilliseconds
|
||||
}.takeIf { it != -1 }?.let { index ->
|
||||
param.setArg(index, 0)
|
||||
onNextActivityCreate {
|
||||
context.mappings.useMapper(ScoreUpdateMapper::class) {
|
||||
classReference.get()?.hookConstructor(HookStage.BEFORE) { param ->
|
||||
param.args().indexOfFirst {
|
||||
val longValue = it.toString().toLongOrNull() ?: return@indexOfFirst false
|
||||
longValue > 30.minutes.inWholeMilliseconds && longValue < 10.days.inWholeMilliseconds
|
||||
}.takeIf { it != -1 }?.let { index ->
|
||||
param.setArg(index, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package me.rhunk.snapenhance.core.features.impl.experiments
|
||||
|
||||
import android.content.Intent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class PreventForcedLogout : Feature("Prevent Forced Logout", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class PreventForcedLogout : Feature("Prevent Forced Logout") {
|
||||
override fun init() {
|
||||
if (!context.config.experimental.preventForcedLogout.get()) return
|
||||
findClass("com.snap.identity.service.ForcedLogoutBroadcastReceiver").hook("onReceive", HookStage.BEFORE) { param ->
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.os.FileObserver
|
||||
import com.google.gson.JsonParser
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.core.util.ktx.setObjectField
|
||||
@@ -13,63 +12,65 @@ import me.rhunk.snapenhance.mapper.impl.DefaultMediaItemMapper
|
||||
import java.io.File
|
||||
|
||||
class BypassVideoLengthRestriction :
|
||||
Feature("BypassVideoLengthRestriction", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
Feature("BypassVideoLengthRestriction") {
|
||||
private lateinit var fileObserver: FileObserver
|
||||
|
||||
override fun asyncOnActivityCreate() {
|
||||
val mode = context.config.global.bypassVideoLengthRestriction.getNullable()
|
||||
override fun init() {
|
||||
onNextActivityCreate(defer = true) {
|
||||
val mode = context.config.global.bypassVideoLengthRestriction.getNullable()
|
||||
|
||||
if (mode == "single") {
|
||||
//fix black videos when story is posted
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val postedStorySnapFolder =
|
||||
File(context.androidContext.filesDir, "file_manager/posted_story_snap")
|
||||
if (mode == "single") {
|
||||
//fix black videos when story is posted
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val postedStorySnapFolder =
|
||||
File(context.androidContext.filesDir, "file_manager/posted_story_snap")
|
||||
|
||||
fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return
|
||||
fileObserver.stopWatching()
|
||||
fileObserver = (object : FileObserver(postedStorySnapFolder, MOVED_TO) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
if (event != MOVED_TO || path?.endsWith("posted_story_snap.2") != true) return
|
||||
fileObserver.stopWatching()
|
||||
|
||||
val file = File(postedStorySnapFolder, path)
|
||||
runCatching {
|
||||
val fileContent = JsonParser.parseReader(file.reader()).asJsonObject
|
||||
if (fileContent["timerOrDuration"].asLong < 0) file.delete()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to read story metadata file", it)
|
||||
val file = File(postedStorySnapFolder, path)
|
||||
runCatching {
|
||||
val fileContent = JsonParser.parseReader(file.reader()).asJsonObject
|
||||
if (fileContent["timerOrDuration"].asLong < 0) file.delete()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to read story metadata file", it)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
|
||||
if (event.destinations.stories!!.isEmpty()) return@subscribe
|
||||
fileObserver.startWatching()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
context.event.subscribe(SendMessageWithContentEvent::class) { event ->
|
||||
if (event.destinations.stories!!.isEmpty()) return@subscribe
|
||||
fileObserver.startWatching()
|
||||
context.mappings.useMapper(DefaultMediaItemMapper::class) {
|
||||
defaultMediaItem.getAsClass()?.hookConstructor(HookStage.BEFORE) { param ->
|
||||
//set the video length argument
|
||||
param.setArg(5, -1L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.mappings.useMapper(DefaultMediaItemMapper::class) {
|
||||
defaultMediaItem.getAsClass()?.hookConstructor(HookStage.BEFORE) { param ->
|
||||
//set the video length argument
|
||||
param.setArg(5, -1L)
|
||||
//TODO: allow split from any source
|
||||
if (mode == "split") {
|
||||
// memories grid
|
||||
context.mappings.useMapper(DefaultMediaItemMapper::class) {
|
||||
cameraRollMediaId.getAsClass()?.hookConstructor(HookStage.AFTER) { param ->
|
||||
//set the durationMs field
|
||||
param.thisObject<Any>()
|
||||
.setObjectField(durationMsField.get()!!, -1L)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: allow split from any source
|
||||
if (mode == "split") {
|
||||
// memories grid
|
||||
context.mappings.useMapper(DefaultMediaItemMapper::class) {
|
||||
cameraRollMediaId.getAsClass()?.hookConstructor(HookStage.AFTER) { param ->
|
||||
//set the durationMs field
|
||||
param.thisObject<Any>()
|
||||
.setObjectField(durationMsField.get()!!, -1L)
|
||||
}
|
||||
}
|
||||
|
||||
// chat camera roll grid
|
||||
findClass("com.snap.composer.memories.MemoriesPickerVideoDurationConfig").hookConstructor(HookStage.AFTER) { param ->
|
||||
param.thisObject<Any>().apply {
|
||||
setObjectField("_maxSingleItemDurationMs", null)
|
||||
setObjectField("_maxTotalDurationMs", null)
|
||||
// chat camera roll grid
|
||||
findClass("com.snap.composer.memories.MemoriesPickerVideoDurationConfig").hookConstructor(HookStage.AFTER) { param ->
|
||||
param.thisObject<Any>().apply {
|
||||
setObjectField("_maxSingleItemDurationMs", null)
|
||||
setObjectField("_maxTotalDurationMs", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ package me.rhunk.snapenhance.core.features.impl.global
|
||||
|
||||
import android.content.Intent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class DisableCustomTabs: Feature("Disable Custom Tabs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class DisableCustomTabs: Feature("Disable Custom Tabs") {
|
||||
override fun init() {
|
||||
if (!context.config.global.disableCustomTabs.get()) return
|
||||
context.mainActivity!!.packageManager.javaClass.hook("resolveService", HookStage.BEFORE) { param ->
|
||||
val intent = param.arg<Intent>(0)
|
||||
if (intent.action == "android.support.customtabs.action.CustomTabsService") {
|
||||
param.setResult(null)
|
||||
onNextActivityCreate { activity ->
|
||||
activity.packageManager.javaClass.hook("resolveService", HookStage.BEFORE) { param ->
|
||||
val intent = param.arg<Intent>(0)
|
||||
if (intent.action == "android.support.customtabs.action.CustomTabsService") {
|
||||
param.setResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.global
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.mapper.impl.MemoriesPresenterMapper
|
||||
|
||||
class DisableMemoriesSnapFeed : Feature("Disable Memories Snap Feed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class DisableMemoriesSnapFeed : Feature("Disable Memories Snap Feed") {
|
||||
override fun init() {
|
||||
if (!context.config.global.disableMemoriesSnapFeed.get()) return
|
||||
onNextActivityCreate {
|
||||
context.mappings.useMapper(MemoriesPresenterMapper::class) {
|
||||
classReference.get()?.apply {
|
||||
val getNameMethod = getMethod("getName") ?: return@apply
|
||||
|
||||
context.mappings.useMapper(MemoriesPresenterMapper::class) {
|
||||
classReference.get()?.apply {
|
||||
val getNameMethod = getMethod("getName") ?: return@apply
|
||||
hook(onNavigationEventMethod.get()!!, HookStage.BEFORE) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
|
||||
hook(onNavigationEventMethod.get()!!, HookStage.BEFORE) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
|
||||
if (getNameMethod.invoke(instance) == "MemoriesAsyncPresenterFragmentSubscriber") {
|
||||
param.setResult(null)
|
||||
if (getNameMethod.invoke(instance) == "MemoriesAsyncPresenterFragmentSubscriber") {
|
||||
param.setResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class DisableMetrics : Feature("DisableMetrics") {
|
||||
override fun init() {
|
||||
if (!context.config.global.disableMetrics.get()) return
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ package me.rhunk.snapenhance.core.features.impl.global
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class DisableTelecomFramework: Feature("Disable Telecom Framework", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class DisableTelecomFramework: Feature("Disable Telecom Framework") {
|
||||
override fun init() {
|
||||
if (!context.config.global.disableTelecomFramework.get()) return
|
||||
|
||||
|
||||
@@ -2,21 +2,22 @@ package me.rhunk.snapenhance.core.features.impl.global
|
||||
|
||||
import android.app.AlertDialog
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
class GooglePlayServicesDialogs : Feature("Disable GMS Dialogs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
override fun asyncOnActivityCreate() {
|
||||
class GooglePlayServicesDialogs : Feature("Disable GMS Dialogs") {
|
||||
override fun init() {
|
||||
if (!context.config.global.disableGooglePlayDialogs.get()) return
|
||||
|
||||
findClass("com.google.android.gms.common.GoogleApiAvailability").methods
|
||||
.first { Modifier.isStatic(it.modifiers) && it.returnType == AlertDialog::class.java }.let { method ->
|
||||
method.hook(HookStage.BEFORE) { param ->
|
||||
context.log.verbose("GoogleApiAvailability.showErrorDialogFragment() called, returning null")
|
||||
param.setResult(null)
|
||||
}
|
||||
onNextActivityCreate(defer = true) {
|
||||
findClass("com.google.android.gms.common.GoogleApiAvailability").methods
|
||||
.first { Modifier.isStatic(it.modifiers) && it.returnType == AlertDialog::class.java }.let { method ->
|
||||
method.hook(HookStage.BEFORE) { param ->
|
||||
context.log.verbose("GoogleApiAvailability.showErrorDialogFragment() called, returning null")
|
||||
param.setResult(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,12 @@ package me.rhunk.snapenhance.core.features.impl.global
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.mapper.impl.MediaQualityLevelProviderMapper
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class MediaUploadQualityOverride : Feature("Media Upload Quality Override", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class MediaUploadQualityOverride : Feature("Media Upload Quality Override") {
|
||||
override fun init() {
|
||||
if (context.config.global.mediaUploadQualityConfig.forceVideoUploadSourceQuality.get()) {
|
||||
context.mappings.useMapper(MediaQualityLevelProviderMapper::class) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.global
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
@@ -9,7 +8,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.core.util.ktx.setObjectField
|
||||
import me.rhunk.snapenhance.mapper.impl.PlusSubscriptionMapper
|
||||
|
||||
class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class SnapchatPlus: Feature("SnapchatPlus") {
|
||||
private val originalSubscriptionTime = (System.currentTimeMillis() - 7776000000L)
|
||||
private val expirationTimeMillis = (System.currentTimeMillis() + 15552000000L)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import me.rhunk.snapenhance.common.data.ContentType
|
||||
import me.rhunk.snapenhance.common.data.MessageUpdate
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
|
||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
||||
import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull
|
||||
@@ -19,7 +18,7 @@ import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.random.Random
|
||||
|
||||
class AutoMarkAsRead : Feature("Auto Mark As Read", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class AutoMarkAsRead : Feature("Auto Mark As Read") {
|
||||
val canMarkConversationAsRead by lazy { context.config.messaging.autoMarkAsRead.get().contains("conversation_read") }
|
||||
|
||||
fun markConversationsAsRead(conversationIds: List<String>) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import me.rhunk.snapenhance.common.data.MessageState
|
||||
import me.rhunk.snapenhance.common.data.MessageUpdate
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.core.event.events.impl.ConversationUpdateEvent
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
|
||||
@@ -16,7 +15,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE) {
|
||||
private val asyncSaveExecutorService = Executors.newSingleThreadExecutor()
|
||||
|
||||
private val messageLogger by lazy { context.feature(MessageLogger::class) }
|
||||
@@ -68,37 +67,39 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
|
||||
return canUseRule(targetConversationId)
|
||||
}
|
||||
|
||||
override fun asyncOnActivityCreate() {
|
||||
// called when enter in a conversation
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
callbacks.getClass("FetchConversationWithMessagesCallback")?.hook(
|
||||
"onFetchConversationWithMessagesComplete",
|
||||
HookStage.BEFORE,
|
||||
{ autoSaveFilter.isNotEmpty() }
|
||||
) { param ->
|
||||
val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!)
|
||||
if (!canSaveInConversation(conversationId.toString())) return@hook
|
||||
override fun init() {
|
||||
onNextActivityCreate(defer = true) {
|
||||
// called when enter in a conversation
|
||||
context.mappings.useMapper(CallbackMapper::class) {
|
||||
callbacks.getClass("FetchConversationWithMessagesCallback")?.hook(
|
||||
"onFetchConversationWithMessagesComplete",
|
||||
HookStage.BEFORE,
|
||||
{ autoSaveFilter.isNotEmpty() }
|
||||
) { param ->
|
||||
val conversationId = SnapUUID(param.arg<Any>(0).getObjectField("mConversationId")!!)
|
||||
if (!canSaveInConversation(conversationId.toString())) return@hook
|
||||
|
||||
val messages = param.arg<List<Any>>(1).map { Message(it) }
|
||||
messages.forEach {
|
||||
if (!canSaveMessage(it)) return@forEach
|
||||
asyncSaveExecutorService.submit {
|
||||
saveMessage(conversationId.toString(), it)
|
||||
val messages = param.arg<List<Any>>(1).map { Message(it) }
|
||||
messages.forEach {
|
||||
if (!canSaveMessage(it)) return@forEach
|
||||
asyncSaveExecutorService.submit {
|
||||
saveMessage(conversationId.toString(), it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(
|
||||
ConversationUpdateEvent::class,
|
||||
{ autoSaveFilter.isNotEmpty() }
|
||||
) { event ->
|
||||
if (!canSaveInConversation(event.conversationId)) return@subscribe
|
||||
context.event.subscribe(
|
||||
ConversationUpdateEvent::class,
|
||||
{ autoSaveFilter.isNotEmpty() }
|
||||
) { event ->
|
||||
if (!canSaveInConversation(event.conversationId)) return@subscribe
|
||||
|
||||
event.messages.forEach { message ->
|
||||
if (!canSaveMessage(message)) return@forEach
|
||||
asyncSaveExecutorService.submit {
|
||||
saveMessage(event.conversationId, message)
|
||||
event.messages.forEach { message ->
|
||||
if (!canSaveMessage(message)) return@forEach
|
||||
asyncSaveExecutorService.submit {
|
||||
saveMessage(event.conversationId, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@ package me.rhunk.snapenhance.core.features.impl.messaging
|
||||
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
class BypassMessageActionRestrictions : Feature("Bypass Message Action Restrictions", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class BypassMessageActionRestrictions : Feature("Bypass Message Action Restrictions") {
|
||||
override fun init() {
|
||||
if (!context.config.messaging.bypassMessageActionRestrictions.get()) return
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 102) { event ->
|
||||
event.message.messageMetadata?.apply {
|
||||
isSaveable = true
|
||||
isReactable = true
|
||||
onNextActivityCreate {
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 102) { event ->
|
||||
event.message.messageMetadata?.apply {
|
||||
isSaveable = true
|
||||
isReactable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
||||
import me.rhunk.snapenhance.core.ui.children
|
||||
import me.rhunk.snapenhance.core.util.hook.HookAdapter
|
||||
@@ -13,7 +12,7 @@ import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.getId
|
||||
|
||||
class CallStartConfirmation : Feature("CallStartConfirmation", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class CallStartConfirmation : Feature("CallStartConfirmation") {
|
||||
private fun hookTouchEvent(param: HookAdapter, motionEvent: MotionEvent, onConfirm: () -> Unit) {
|
||||
if (motionEvent.action != MotionEvent.ACTION_UP) return
|
||||
param.setResult(true)
|
||||
@@ -26,37 +25,39 @@ class CallStartConfirmation : Feature("CallStartConfirmation", loadParams = Feat
|
||||
}
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (!context.config.messaging.callStartConfirmation.get()) return
|
||||
|
||||
val callButtonsStub = context.resources.getId("call_buttons_stub")
|
||||
onNextActivityCreate {
|
||||
val callButtonsStub = context.resources.getId("call_buttons_stub")
|
||||
|
||||
findClass("com.snap.composer.views.ComposerRootView").hook("dispatchTouchEvent", HookStage.BEFORE) { param ->
|
||||
val view = param.thisObject() as? ViewGroup ?: return@hook
|
||||
if (view.id != callButtonsStub) return@hook
|
||||
val childComposerView = view.getChildAt(0) as? ViewGroup ?: return@hook
|
||||
// check if the child composer view contains 2 call buttons
|
||||
if (childComposerView.children().count {
|
||||
it::class.java == childComposerView::class.java
|
||||
} != 2) return@hook
|
||||
hookTouchEvent(param, param.arg(0)) {
|
||||
param.invokeOriginal()
|
||||
findClass("com.snap.composer.views.ComposerRootView").hook("dispatchTouchEvent", HookStage.BEFORE) { param ->
|
||||
val view = param.thisObject() as? ViewGroup ?: return@hook
|
||||
if (view.id != callButtonsStub) return@hook
|
||||
val childComposerView = view.getChildAt(0) as? ViewGroup ?: return@hook
|
||||
// check if the child composer view contains 2 call buttons
|
||||
if (childComposerView.children().count {
|
||||
it::class.java == childComposerView::class.java
|
||||
} != 2) return@hook
|
||||
hookTouchEvent(param, param.arg(0)) {
|
||||
param.invokeOriginal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val callButton1 = context.resources.getId("friend_action_button3")
|
||||
val callButton2 = context.resources.getId("friend_action_button4")
|
||||
val callButton1 = context.resources.getId("friend_action_button3")
|
||||
val callButton2 = context.resources.getId("friend_action_button4")
|
||||
|
||||
findClass("com.snap.ui.view.stackdraw.StackDrawLayout").hook("onTouchEvent", HookStage.BEFORE) { param ->
|
||||
val view = param.thisObject<View>()
|
||||
if (view.id != callButton1 && view.id != callButton2) return@hook
|
||||
findClass("com.snap.ui.view.stackdraw.StackDrawLayout").hook("onTouchEvent", HookStage.BEFORE) { param ->
|
||||
val view = param.thisObject<View>()
|
||||
if (view.id != callButton1 && view.id != callButton2) return@hook
|
||||
|
||||
hookTouchEvent(param, param.arg(0)) {
|
||||
arrayOf(
|
||||
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0),
|
||||
MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0)
|
||||
).forEach {
|
||||
param.invokeOriginal(arrayOf(it))
|
||||
hookTouchEvent(param, param.arg(0)) {
|
||||
arrayOf(
|
||||
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0),
|
||||
MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0)
|
||||
).forEach {
|
||||
param.invokeOriginal(arrayOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.messaging
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.core.util.ktx.setEnumField
|
||||
|
||||
class DisableReplayInFF : Feature("DisableReplayInFF", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
override fun asyncOnActivityCreate() {
|
||||
class DisableReplayInFF : Feature("DisableReplayInFF") {
|
||||
override fun init() {
|
||||
val state by context.config.messaging.disableReplayInFF
|
||||
|
||||
findClass("com.snapchat.client.messaging.InteractionInfo")
|
||||
.hookConstructor(HookStage.AFTER, { state }) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
if (instance.getObjectField("mLongPressActionState").toString() == "REQUEST_SNAP_REPLAY") {
|
||||
instance.setEnumField("mLongPressActionState", "SHOW_CONVERSATION_ACTION_MENU")
|
||||
}
|
||||
onNextActivityCreate(defer = true) {
|
||||
findClass("com.snapchat.client.messaging.InteractionInfo")
|
||||
.hookConstructor(HookStage.AFTER, { state }) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
if (instance.getObjectField("mLongPressActionState").toString() == "REQUEST_SNAP_REPLAY") {
|
||||
instance.setEnumField("mLongPressActionState", "SHOW_CONVERSATION_ACTION_MENU")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import me.rhunk.snapenhance.common.ReceiversConfig
|
||||
import me.rhunk.snapenhance.core.event.events.impl.ConversationUpdateEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.OnSnapInteractionEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
|
||||
import me.rhunk.snapenhance.core.util.EvictingMap
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
@@ -25,7 +24,7 @@ import me.rhunk.snapenhance.mapper.impl.FriendsFeedEventDispatcherMapper
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Future
|
||||
|
||||
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
|
||||
class Messaging : Feature("Messaging") {
|
||||
var conversationManager: ConversationManager? = null
|
||||
private set
|
||||
private var conversationManagerDelegate: Any? = null
|
||||
@@ -48,6 +47,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
val stealthMode = context.feature(StealthMode::class)
|
||||
context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param ->
|
||||
conversationManager = ConversationManager(context, param.thisObject())
|
||||
context.messagingBridge.triggerSessionStart()
|
||||
@@ -83,6 +83,80 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer {
|
||||
arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook ->
|
||||
context.classCache.presenceSession.hook(hook, HookStage.BEFORE, {
|
||||
context.config.messaging.hideBitmojiPresence.get() || stealthMode.canUseRule(openedConversationUUID.toString())
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, {
|
||||
context.config.messaging.hidePeekAPeek.get() || stealthMode.canUseRule(openedConversationUUID.toString())
|
||||
}) { it.setResult(null) }
|
||||
|
||||
//get last opened snap for media downloader
|
||||
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
|
||||
openedConversationUUID = event.conversationId
|
||||
lastFocusedMessageId = event.messageId
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.hook("fetchMessage", HookStage.BEFORE) { param ->
|
||||
val conversationId = SnapUUID(param.arg(0)).toString()
|
||||
if (openedConversationUUID?.toString() == conversationId) {
|
||||
lastFocusedMessageId = param.arg(1)
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, {
|
||||
context.config.messaging.hideTypingNotifications.get() || stealthMode.canUseRule(openedConversationUUID.toString())
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
onNextActivityCreate {
|
||||
context.mappings.useMapper(FriendsFeedEventDispatcherMapper::class) {
|
||||
classReference.getAsClass()?.hook("onItemLongPress", HookStage.BEFORE) { param ->
|
||||
val viewItemContainer = param.arg<Any>(0)
|
||||
val viewItem = viewItemContainer.getObjectField(viewModelField.get()!!).toString()
|
||||
val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also {
|
||||
if (it.startsWith("null")) return@hook
|
||||
}
|
||||
lastFocusedConversationId = conversationId
|
||||
lastFocusedConversationType = context.database.getConversationType(conversationId) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor
|
||||
val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor
|
||||
val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString()
|
||||
val myUserId = context.database.myUserId
|
||||
|
||||
feedCachedSnapMessages[conversationId] = messages.filter { msg ->
|
||||
msg.messageMetadata?.openedBy?.none { it.toString() == myUserId } == true
|
||||
}.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId }
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.apply {
|
||||
hook("enterConversation", HookStage.BEFORE) { param ->
|
||||
openedConversationUUID = SnapUUID(param.arg(0))
|
||||
if (context.config.messaging.bypassMessageRetentionPolicy.get()) {
|
||||
val callback = param.argNullable<Any>(2) ?: return@hook
|
||||
callback::class.java.methods.firstOrNull { it.name == "onSuccess" }?.invoke(callback)
|
||||
param.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
hook("exitConversation", HookStage.BEFORE) {
|
||||
openedConversationUUID = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFeedCachedMessageIds(conversationId: String) = feedCachedSnapMessages[conversationId]
|
||||
@@ -116,82 +190,6 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
context.mappings.useMapper(FriendsFeedEventDispatcherMapper::class) {
|
||||
classReference.getAsClass()?.hook("onItemLongPress", HookStage.BEFORE) { param ->
|
||||
val viewItemContainer = param.arg<Any>(0)
|
||||
val viewItem = viewItemContainer.getObjectField(viewModelField.get()!!).toString()
|
||||
val conversationId = viewItem.substringAfter("conversationId: ").substring(0, 36).also {
|
||||
if (it.startsWith("null")) return@hook
|
||||
}
|
||||
lastFocusedConversationId = conversationId
|
||||
lastFocusedConversationType = context.database.getConversationType(conversationId) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.feedEntry.hookConstructor(HookStage.AFTER) { param ->
|
||||
val instance = param.thisObject<Any>()
|
||||
val interactionInfo = instance.getObjectFieldOrNull("mInteractionInfo") ?: return@hookConstructor
|
||||
val messages = (interactionInfo.getObjectFieldOrNull("mMessages") as? List<*>)?.map { Message(it) } ?: return@hookConstructor
|
||||
val conversationId = SnapUUID(instance.getObjectFieldOrNull("mConversationId") ?: return@hookConstructor).toString()
|
||||
val myUserId = context.database.myUserId
|
||||
|
||||
feedCachedSnapMessages[conversationId] = messages.filter { msg ->
|
||||
msg.messageMetadata?.openedBy?.none { it.toString() == myUserId } == true
|
||||
}.sortedBy { it.orderKey }.mapNotNull { it.messageDescriptor?.messageId }
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.apply {
|
||||
hook("enterConversation", HookStage.BEFORE) { param ->
|
||||
openedConversationUUID = SnapUUID(param.arg(0))
|
||||
if (context.config.messaging.bypassMessageRetentionPolicy.get()) {
|
||||
val callback = param.argNullable<Any>(2) ?: return@hook
|
||||
callback::class.java.methods.firstOrNull { it.name == "onSuccess" }?.invoke(callback)
|
||||
param.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
hook("exitConversation", HookStage.BEFORE) {
|
||||
openedConversationUUID = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun asyncInit() {
|
||||
val stealthMode = context.feature(StealthMode::class)
|
||||
|
||||
arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook ->
|
||||
context.classCache.presenceSession.hook(hook, HookStage.BEFORE, {
|
||||
context.config.messaging.hideBitmojiPresence.get() || stealthMode.canUseRule(openedConversationUUID.toString())
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, {
|
||||
context.config.messaging.hidePeekAPeek.get() || stealthMode.canUseRule(openedConversationUUID.toString())
|
||||
}) { it.setResult(null) }
|
||||
|
||||
//get last opened snap for media downloader
|
||||
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
|
||||
openedConversationUUID = event.conversationId
|
||||
lastFocusedMessageId = event.messageId
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.hook("fetchMessage", HookStage.BEFORE) { param ->
|
||||
val conversationId = SnapUUID(param.arg(0)).toString()
|
||||
if (openedConversationUUID?.toString() == conversationId) {
|
||||
lastFocusedMessageId = param.arg(1)
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, {
|
||||
context.config.messaging.hideTypingNotifications.get() || stealthMode.canUseRule(openedConversationUUID.toString())
|
||||
}) {
|
||||
it.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchSnapchatterInfos(userIds: List<String>): List<Snapchatter> {
|
||||
val identity = identityDelegate ?: return emptyList()
|
||||
val snapUUIDs = userIds.map {
|
||||
|
||||
@@ -23,7 +23,6 @@ import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper
|
||||
import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.FriendMutationObserver
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.decoder.AttachmentType
|
||||
@@ -39,7 +38,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class Notifications : Feature("Notifications") {
|
||||
inner class NotificationData(
|
||||
val tag: String?,
|
||||
val id: Int,
|
||||
@@ -301,7 +300,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.warn("Failed to set notification group key: ${it.stackTraceToString()}", featureKey)
|
||||
context.log.warn("Failed to set notification group key: ${it.stackTraceToString()}", key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class PreventMessageSending : Feature("Prevent message sending", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
override fun asyncOnActivityCreate() {
|
||||
class PreventMessageSending : Feature("Prevent message sending") {
|
||||
override fun init() {
|
||||
val preventMessageSending by context.config.messaging.preventMessageSending
|
||||
|
||||
context.event.subscribe(NativeUnaryCallEvent::class, { preventMessageSending.contains("snap_replay") }) { event ->
|
||||
|
||||
@@ -8,14 +8,13 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.experiments.MediaFilePicker
|
||||
import me.rhunk.snapenhance.core.messaging.MessageSender
|
||||
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
|
||||
import me.rhunk.snapenhance.nativelib.NativeLib
|
||||
import java.util.Locale
|
||||
|
||||
class SendOverride : Feature("Send Override", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class SendOverride : Feature("Send Override") {
|
||||
private var isLastSnapSavable = false
|
||||
private val typeNames by lazy {
|
||||
mutableListOf("ORIGINAL", "SNAP", "NOTE").also {
|
||||
|
||||
@@ -6,27 +6,27 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
class UnlimitedSnapViewTime :
|
||||
Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
val state by context.config.messaging.unlimitedSnapViewTime
|
||||
class UnlimitedSnapViewTime : Feature("UnlimitedSnapViewTime") {
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
val state by context.config.messaging.unlimitedSnapViewTime
|
||||
|
||||
context.event.subscribe(BuildMessageEvent::class, { state }, priority = 101) { event ->
|
||||
if (event.message.messageState != MessageState.COMMITTED) return@subscribe
|
||||
if (event.message.messageContent!!.contentType != ContentType.SNAP) return@subscribe
|
||||
context.event.subscribe(BuildMessageEvent::class, { state }, priority = 101) { event ->
|
||||
if (event.message.messageState != MessageState.COMMITTED) return@subscribe
|
||||
if (event.message.messageContent!!.contentType != ContentType.SNAP) return@subscribe
|
||||
|
||||
val messageContent = event.message.messageContent
|
||||
val messageContent = event.message.messageContent
|
||||
|
||||
val mediaAttributes = ProtoReader(messageContent!!.content!!).followPath(11, 5, 2) ?: return@subscribe
|
||||
if (mediaAttributes.contains(6)) return@subscribe
|
||||
messageContent.content = ProtoEditor(messageContent.content!!).apply {
|
||||
edit(11, 5, 2) {
|
||||
remove(8)
|
||||
addBuffer(6, byteArrayOf())
|
||||
}
|
||||
}.toByteArray()
|
||||
val mediaAttributes = ProtoReader(messageContent!!.content!!).followPath(11, 5, 2) ?: return@subscribe
|
||||
if (mediaAttributes.contains(6)) return@subscribe
|
||||
messageContent.content = ProtoEditor(messageContent.content!!).apply {
|
||||
edit(11, 5, 2) {
|
||||
remove(8)
|
||||
addBuffer(6, byteArrayOf())
|
||||
}
|
||||
}.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import me.rhunk.snapenhance.common.util.lazyBridge
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.common.util.toParcelable
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
@@ -23,7 +22,7 @@ import me.rhunk.snapenhance.nativelib.NativeLib
|
||||
import java.lang.reflect.Method
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class FriendTracker : Feature("Friend Tracker", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class FriendTracker : Feature("Friend Tracker") {
|
||||
private val conversationPresenceState = mutableMapOf<String, MutableMap<String, FriendPresenceState?>>() // conversationId -> (userId -> state)
|
||||
private val tracker by lazyBridge { context.bridgeClient.getTracker() }
|
||||
private val notificationManager by lazy { context.androidContext.getSystemService(NotificationManager::class.java).apply {
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import me.rhunk.snapenhance.common.Constants
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
@@ -16,7 +15,7 @@ import me.rhunk.snapenhance.mapper.impl.CallbackMapper
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class HalfSwipeNotifier : Feature("Half Swipe Notifier", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class HalfSwipeNotifier : Feature("Half Swipe Notifier") {
|
||||
private val peekingConversations = ConcurrentHashMap<String, List<String>>()
|
||||
private val startPeekingTimestamps = ConcurrentHashMap<String, Long>()
|
||||
|
||||
|
||||
@@ -18,18 +18,13 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
||||
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
||||
import me.rhunk.snapenhance.core.util.EvictingMap
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class MessageLogger : Feature("MessageLogger",
|
||||
loadParams = FeatureLoadParams.INIT_SYNC or
|
||||
FeatureLoadParams.ACTIVITY_CREATE_SYNC or
|
||||
FeatureLoadParams.ACTIVITY_CREATE_ASYNC
|
||||
) {
|
||||
class MessageLogger : Feature("MessageLogger") {
|
||||
companion object {
|
||||
const val PREFETCH_MESSAGE_COUNT = 20
|
||||
const val PREFETCH_FEED_COUNT = 20
|
||||
@@ -96,23 +91,20 @@ class MessageLogger : Feature("MessageLogger",
|
||||
return computeMessageIdentifier(conversationId, serverMessageId)
|
||||
}
|
||||
|
||||
override fun asyncOnActivityCreate() {
|
||||
if (!isEnabled || !context.database.hasArroyo()) {
|
||||
return
|
||||
}
|
||||
|
||||
measureTimeMillis {
|
||||
val conversationIds = context.database.getFeedEntries(PREFETCH_FEED_COUNT).map { it.key!! }
|
||||
if (conversationIds.isEmpty()) return@measureTimeMillis
|
||||
fetchedMessages.addAll(loggerInterface.getLoggedIds(conversationIds.toTypedArray(), PREFETCH_MESSAGE_COUNT).toList())
|
||||
}.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in ${it}ms") }
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
if (!isEnabled) return
|
||||
val keepMyOwnMessages = context.config.messaging.messageLogger.keepMyOwnMessages.get()
|
||||
val messageFilter by context.config.messaging.messageLogger.messageFilter
|
||||
|
||||
onNextActivityCreate(defer = true) {
|
||||
if (!context.database.hasArroyo()) return@onNextActivityCreate
|
||||
measureTimeMillis {
|
||||
val conversationIds = context.database.getFeedEntries(PREFETCH_FEED_COUNT).map { it.key!! }
|
||||
if (conversationIds.isEmpty()) return@measureTimeMillis
|
||||
fetchedMessages.addAll(loggerInterface.getLoggedIds(conversationIds.toTypedArray(), PREFETCH_MESSAGE_COUNT).toList())
|
||||
}.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in ${it}ms") }
|
||||
}
|
||||
|
||||
context.event.subscribe(BuildMessageEvent::class, priority = 1) { event ->
|
||||
val messageInstance = event.message.instanceNonNull()
|
||||
if (event.message.messageState != MessageState.COMMITTED) return@subscribe
|
||||
@@ -183,10 +175,6 @@ class MessageLogger : Feature("MessageLogger",
|
||||
|
||||
deletedMessageCache[uniqueMessageIdentifier] = deletedMessageObject
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
if (!isEnabled) return
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { conversationId, messageId ->
|
||||
|
||||
@@ -2,14 +2,13 @@ package me.rhunk.snapenhance.core.features.impl.spying
|
||||
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.core.event.events.impl.OnSnapInteractionEvent
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH, loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH) {
|
||||
private val displayedMessageQueue = CopyOnWriteArraySet<Long>()
|
||||
|
||||
fun addDisplayedMessageException(clientMessageId: Long) {
|
||||
|
||||
@@ -5,12 +5,11 @@ import android.content.ContentResolver
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class BypassScreenshotDetection : Feature("BypassScreenshotDetection", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class BypassScreenshotDetection : Feature("BypassScreenshotDetection") {
|
||||
override fun init() {
|
||||
if (!context.config.messaging.bypassScreenshotDetection.get()) return
|
||||
Activity::class.java.hook("registerScreenCaptureCallback", HookStage.BEFORE) { param ->
|
||||
param.setResult(null)
|
||||
|
||||
@@ -12,100 +12,101 @@ import android.media.Image
|
||||
import android.media.ImageReader
|
||||
import android.util.Range
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.setObjectField
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class CameraTweaks : Feature("Camera Tweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class CameraTweaks : Feature("Camera Tweaks") {
|
||||
|
||||
private fun parseResolution(resolution: String): IntArray? {
|
||||
return runCatching { resolution.split("x").map { it.toInt() }.toIntArray() }.getOrNull()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission", "DiscouragedApi")
|
||||
override fun onActivityCreate() {
|
||||
val config = context.config.camera
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
val config = context.config.camera
|
||||
|
||||
val frontCameraId = runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
|
||||
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT }
|
||||
} }.getOrNull()
|
||||
val frontCameraId = runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
|
||||
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT }
|
||||
} }.getOrNull()
|
||||
|
||||
if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) {
|
||||
ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param ->
|
||||
val permission = param.arg<String>(0)
|
||||
if (permission == Manifest.permission.CAMERA) {
|
||||
param.setResult(PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isLastCameraFront = false
|
||||
|
||||
CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param ->
|
||||
val cameraManager = param.thisObject() as? CameraManager ?: return@hook
|
||||
val cameraId = param.arg<String>(0)
|
||||
val disabledCameras = config.disableCameras.get()
|
||||
|
||||
if (disabledCameras.size >= 2) {
|
||||
param.setResult(null)
|
||||
return@hook
|
||||
}
|
||||
|
||||
isLastCameraFront = cameraId == frontCameraId
|
||||
|
||||
if (disabledCameras.size != 1) return@hook
|
||||
|
||||
// trick to replace unwanted camera with another one
|
||||
if ((disabledCameras.contains("front") && isLastCameraFront) || (disabledCameras.contains("back") && !isLastCameraFront)) {
|
||||
param.setArg(0, cameraManager.cameraIdList.filterNot { it == cameraId }.firstOrNull() ?: return@hook)
|
||||
isLastCameraFront = !isLastCameraFront
|
||||
}
|
||||
}
|
||||
|
||||
ImageReader::class.java.hook("newInstance", HookStage.BEFORE) { param ->
|
||||
val captureResolutionConfig = config.customResolution.getNullable()?.takeIf { it.isNotEmpty() }?.let { parseResolution(it) }
|
||||
?: (if (isLastCameraFront) config.overrideFrontResolution.getNullable() else config.overrideBackResolution.getNullable())?.let { parseResolution(it) } ?: return@hook
|
||||
param.setArg(0, captureResolutionConfig[0])
|
||||
param.setArg(1, captureResolutionConfig[1])
|
||||
}
|
||||
|
||||
CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param ->
|
||||
val key = param.argNullable<Key<*>>(0) ?: return@hook
|
||||
|
||||
if (key == CameraCharacteristics.LENS_FACING) {
|
||||
val disabledCameras = config.disableCameras.get()
|
||||
//FIXME: unexpected behavior when app is resumed
|
||||
if (disabledCameras.size == 1) {
|
||||
val isFrontCamera = param.getResult() as? Int == CameraCharacteristics.LENS_FACING_FRONT
|
||||
if ((disabledCameras.contains("front") && isFrontCamera) || (disabledCameras.contains("back") && !isFrontCamera)) {
|
||||
param.setResult(if (isFrontCamera) CameraCharacteristics.LENS_FACING_BACK else CameraCharacteristics.LENS_FACING_FRONT)
|
||||
if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) {
|
||||
ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param ->
|
||||
val permission = param.arg<String>(0)
|
||||
if (permission == Manifest.permission.CAMERA) {
|
||||
param.setResult(PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) {
|
||||
val isFrontCamera = param.invokeOriginal(
|
||||
arrayOf(CameraCharacteristics.LENS_FACING)
|
||||
) == CameraCharacteristics.LENS_FACING_FRONT
|
||||
val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook
|
||||
param.setResult(arrayOf(Range(customFrameRate, customFrameRate)))
|
||||
}
|
||||
}
|
||||
var isLastCameraFront = false
|
||||
|
||||
if (config.blackPhotos.get()) {
|
||||
findClass("android.media.ImageReader\$SurfaceImage").hook("getPlanes", HookStage.AFTER) { param ->
|
||||
val image = param.thisObject() as? Image ?: return@hook
|
||||
val planes = param.getResult() as? Array<*> ?: return@hook
|
||||
val output = ByteArrayOutputStream()
|
||||
Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888).apply {
|
||||
compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
recycle()
|
||||
CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param ->
|
||||
val cameraManager = param.thisObject() as? CameraManager ?: return@hook
|
||||
val cameraId = param.arg<String>(0)
|
||||
val disabledCameras = config.disableCameras.get()
|
||||
|
||||
if (disabledCameras.size >= 2) {
|
||||
param.setResult(null)
|
||||
return@hook
|
||||
}
|
||||
planes.filterNotNull().forEach { plane ->
|
||||
plane.setObjectField("mBuffer", ByteBuffer.wrap(output.toByteArray()))
|
||||
|
||||
isLastCameraFront = cameraId == frontCameraId
|
||||
|
||||
if (disabledCameras.size != 1) return@hook
|
||||
|
||||
// trick to replace unwanted camera with another one
|
||||
if ((disabledCameras.contains("front") && isLastCameraFront) || (disabledCameras.contains("back") && !isLastCameraFront)) {
|
||||
param.setArg(0, cameraManager.cameraIdList.filterNot { it == cameraId }.firstOrNull() ?: return@hook)
|
||||
isLastCameraFront = !isLastCameraFront
|
||||
}
|
||||
}
|
||||
|
||||
ImageReader::class.java.hook("newInstance", HookStage.BEFORE) { param ->
|
||||
val captureResolutionConfig = config.customResolution.getNullable()?.takeIf { it.isNotEmpty() }?.let { parseResolution(it) }
|
||||
?: (if (isLastCameraFront) config.overrideFrontResolution.getNullable() else config.overrideBackResolution.getNullable())?.let { parseResolution(it) } ?: return@hook
|
||||
param.setArg(0, captureResolutionConfig[0])
|
||||
param.setArg(1, captureResolutionConfig[1])
|
||||
}
|
||||
|
||||
CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param ->
|
||||
val key = param.argNullable<Key<*>>(0) ?: return@hook
|
||||
|
||||
if (key == CameraCharacteristics.LENS_FACING) {
|
||||
val disabledCameras = config.disableCameras.get()
|
||||
//FIXME: unexpected behavior when app is resumed
|
||||
if (disabledCameras.size == 1) {
|
||||
val isFrontCamera = param.getResult() as? Int == CameraCharacteristics.LENS_FACING_FRONT
|
||||
if ((disabledCameras.contains("front") && isFrontCamera) || (disabledCameras.contains("back") && !isFrontCamera)) {
|
||||
param.setResult(if (isFrontCamera) CameraCharacteristics.LENS_FACING_BACK else CameraCharacteristics.LENS_FACING_FRONT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) {
|
||||
val isFrontCamera = param.invokeOriginal(
|
||||
arrayOf(CameraCharacteristics.LENS_FACING)
|
||||
) == CameraCharacteristics.LENS_FACING_FRONT
|
||||
val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook
|
||||
param.setResult(arrayOf(Range(customFrameRate, customFrameRate)))
|
||||
}
|
||||
}
|
||||
|
||||
if (config.blackPhotos.get()) {
|
||||
findClass("android.media.ImageReader\$SurfaceImage").hook("getPlanes", HookStage.AFTER) { param ->
|
||||
val image = param.thisObject() as? Image ?: return@hook
|
||||
val planes = param.getResult() as? Array<*> ?: return@hook
|
||||
val output = ByteArrayOutputStream()
|
||||
Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888).apply {
|
||||
compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
recycle()
|
||||
}
|
||||
planes.filterNotNull().forEach { plane ->
|
||||
plane.setObjectField("mBuffer", ByteBuffer.wrap(output.toByteArray()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import android.content.ContextWrapper
|
||||
import android.content.pm.PackageManager
|
||||
import me.rhunk.snapenhance.common.config.impl.Global
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class DisablePermissionRequests : Feature("Disable Permission Requests", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class DisablePermissionRequests : Feature("Disable Permission Requests") {
|
||||
override fun init() {
|
||||
val deniedPermissions by context.config.global.disablePermissionRequests
|
||||
if (deniedPermissions.isEmpty()) return
|
||||
|
||||
@@ -2,15 +2,16 @@ package me.rhunk.snapenhance.core.features.impl.tweaks
|
||||
|
||||
import android.media.AudioManager
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class HideActiveMusic: Feature("Hide Active Music", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class HideActiveMusic: Feature("Hide Active Music") {
|
||||
override fun init() {
|
||||
if (!context.config.global.hideActiveMusic.get()) return
|
||||
AudioManager::class.java.hook("isMusicActive", HookStage.BEFORE) {
|
||||
it.setResult(false)
|
||||
onNextActivityCreate {
|
||||
AudioManager::class.java.hook("isMusicActive", HookStage.BEFORE) {
|
||||
it.setResult(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,73 +4,74 @@ import android.view.View
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.ConversationUpdateEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
|
||||
class PreventMessageListAutoScroll : Feature("PreventMessageListAutoScroll", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class PreventMessageListAutoScroll : Feature("PreventMessageListAutoScroll") {
|
||||
private var openedConversationId: String? = null
|
||||
private val focusedMessages = mutableMapOf<View, Long>()
|
||||
private var firstFocusedMessageId: Long? = null
|
||||
private val delayedMessageUpdates = mutableListOf<() -> Unit>()
|
||||
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (!context.config.userInterface.preventMessageListAutoScroll.get()) return
|
||||
|
||||
context.event.subscribe(ConversationUpdateEvent::class) { event ->
|
||||
val updatedMessage = event.messages.firstOrNull() ?: return@subscribe
|
||||
if (openedConversationId != updatedMessage.messageDescriptor?.conversationId.toString()) return@subscribe
|
||||
onNextActivityCreate {
|
||||
context.event.subscribe(ConversationUpdateEvent::class) { event ->
|
||||
val updatedMessage = event.messages.firstOrNull() ?: return@subscribe
|
||||
if (openedConversationId != updatedMessage.messageDescriptor?.conversationId.toString()) return@subscribe
|
||||
|
||||
// cancel if the message is already in focus
|
||||
if (focusedMessages.entries.any { entry -> entry.value == updatedMessage.messageDescriptor?.messageId && entry.key.isAttachedToWindow }) return@subscribe
|
||||
// cancel if the message is already in focus
|
||||
if (focusedMessages.entries.any { entry -> entry.value == updatedMessage.messageDescriptor?.messageId && entry.key.isAttachedToWindow }) return@subscribe
|
||||
|
||||
val conversationLastMessages = context.database.getMessagesFromConversationId(
|
||||
openedConversationId.toString(),
|
||||
4
|
||||
) ?: return@subscribe
|
||||
val conversationLastMessages = context.database.getMessagesFromConversationId(
|
||||
openedConversationId.toString(),
|
||||
4
|
||||
) ?: return@subscribe
|
||||
|
||||
if (conversationLastMessages.none {
|
||||
focusedMessages.entries.any { entry -> entry.value == it.clientMessageId.toLong() && entry.key.isAttachedToWindow }
|
||||
}) {
|
||||
synchronized(delayedMessageUpdates) {
|
||||
if (firstFocusedMessageId == null) firstFocusedMessageId = conversationLastMessages.lastOrNull()?.clientMessageId?.toLong()
|
||||
delayedMessageUpdates.add {
|
||||
event.adapter.invokeOriginal()
|
||||
if (conversationLastMessages.none {
|
||||
focusedMessages.entries.any { entry -> entry.value == it.clientMessageId.toLong() && entry.key.isAttachedToWindow }
|
||||
}) {
|
||||
synchronized(delayedMessageUpdates) {
|
||||
if (firstFocusedMessageId == null) firstFocusedMessageId = conversationLastMessages.lastOrNull()?.clientMessageId?.toLong()
|
||||
delayedMessageUpdates.add {
|
||||
event.adapter.invokeOriginal()
|
||||
}
|
||||
}
|
||||
event.adapter.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.apply {
|
||||
hook("enterConversation", HookStage.BEFORE) { param ->
|
||||
openedConversationId = SnapUUID(param.arg(0)).toString()
|
||||
}
|
||||
hook("exitConversation", HookStage.BEFORE) {
|
||||
openedConversationId = null
|
||||
firstFocusedMessageId = null
|
||||
synchronized(focusedMessages) {
|
||||
focusedMessages.clear()
|
||||
}
|
||||
synchronized(delayedMessageUpdates) {
|
||||
delayedMessageUpdates.clear()
|
||||
}
|
||||
}
|
||||
event.adapter.setResult(null)
|
||||
}
|
||||
}
|
||||
|
||||
context.classCache.conversationManager.apply {
|
||||
hook("enterConversation", HookStage.BEFORE) { param ->
|
||||
openedConversationId = SnapUUID(param.arg(0)).toString()
|
||||
}
|
||||
hook("exitConversation", HookStage.BEFORE) {
|
||||
openedConversationId = null
|
||||
firstFocusedMessageId = null
|
||||
synchronized(focusedMessages) {
|
||||
focusedMessages.clear()
|
||||
}
|
||||
synchronized(delayedMessageUpdates) {
|
||||
delayedMessageUpdates.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { conversationId, messageId ->
|
||||
if (conversationId != openedConversationId) return@chatMessage
|
||||
synchronized(focusedMessages) {
|
||||
focusedMessages[event.view] = messageId.toLong()
|
||||
}
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { conversationId, messageId ->
|
||||
if (conversationId != openedConversationId) return@chatMessage
|
||||
synchronized(focusedMessages) {
|
||||
focusedMessages[event.view] = messageId.toLong()
|
||||
}
|
||||
|
||||
if (delayedMessageUpdates.isNotEmpty() && focusedMessages.entries.any { entry -> entry.value == firstFocusedMessageId && entry.key.isAttachedToWindow }) {
|
||||
delayedMessageUpdates.apply {
|
||||
synchronized(this) {
|
||||
removeIf { it(); true }
|
||||
firstFocusedMessageId = null
|
||||
if (delayedMessageUpdates.isNotEmpty() && focusedMessages.entries.any { entry -> entry.value == firstFocusedMessageId && entry.key.isAttachedToWindow }) {
|
||||
delayedMessageUpdates.apply {
|
||||
synchronized(this) {
|
||||
removeIf { it(); true }
|
||||
firstFocusedMessageId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.tweaks
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
|
||||
class RemoveGroupsLockedStatus : Feature("Remove Groups Locked Status", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class RemoveGroupsLockedStatus : Feature("Remove Groups Locked Status") {
|
||||
override fun init() {
|
||||
if (!context.config.messaging.removeGroupsLockedStatus.get()) return
|
||||
context.classCache.conversation.hookConstructor(HookStage.AFTER) { param ->
|
||||
param.thisObject<Any>().dataBuilder {
|
||||
set("mLockedState", "UNLOCKED")
|
||||
onNextActivityCreate(defer = true) {
|
||||
context.classCache.conversation.hookConstructor(HookStage.AFTER) { param ->
|
||||
param.thisObject<Any>().dataBuilder {
|
||||
set("mLockedState", "UNLOCKED")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
|
||||
class UnsaveableMessages : MessagingRuleFeature(
|
||||
"Unsaveable Messages",
|
||||
MessagingRuleType.UNSAVEABLE_MESSAGES,
|
||||
loadParams = FeatureLoadParams.INIT_SYNC
|
||||
MessagingRuleType.UNSAVEABLE_MESSAGES
|
||||
) {
|
||||
override fun init() {
|
||||
if (context.config.rules.getRuleState(MessagingRuleType.UNSAVEABLE_MESSAGES) == null) return
|
||||
|
||||
@@ -2,11 +2,10 @@ package me.rhunk.snapenhance.core.features.impl.ui
|
||||
|
||||
import me.rhunk.snapenhance.common.config.impl.UserInterfaceTweaks
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import java.io.File
|
||||
|
||||
|
||||
class ClientBootstrapOverride: Feature("ClientBootstrapOverride", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class ClientBootstrapOverride: Feature("ClientBootstrapOverride") {
|
||||
|
||||
private val clientBootstrapFolder by lazy { File(context.androidContext.filesDir, "client-bootstrap") }
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface
|
||||
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import me.rhunk.snapenhance.core.util.ktx.getId
|
||||
|
||||
@@ -45,7 +44,7 @@ data class ComposableMenu(
|
||||
val composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit,
|
||||
)
|
||||
|
||||
class ConversationToolbox : Feature("Conversation Toolbox", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class ConversationToolbox : Feature("Conversation Toolbox") {
|
||||
private val composableList = mutableListOf<ComposableMenu>()
|
||||
private val expandedComposableCache = mutableStateMapOf<String, Boolean>()
|
||||
|
||||
@@ -56,49 +55,51 @@ class ConversationToolbox : Feature("Conversation Toolbox", loadParams = Feature
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onActivityCreate() {
|
||||
val defaultInputBarId = context.resources.getId("default_input_bar")
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
val defaultInputBarId = context.resources.getId("default_input_bar")
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view.id != defaultInputBarId) return@subscribe
|
||||
if (composableList.isEmpty()) return@subscribe
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.view.id != defaultInputBarId) return@subscribe
|
||||
if (composableList.isEmpty()) return@subscribe
|
||||
|
||||
(event.view as ViewGroup).addView(FrameLayout(event.view.context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
(52 * context.resources.displayMetrics.density).toInt(),
|
||||
).apply {
|
||||
gravity = Gravity.BOTTOM
|
||||
}
|
||||
setPadding(25, 0, 25, 0)
|
||||
|
||||
addView(TextView(event.view.context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
(event.view as ViewGroup).addView(FrameLayout(event.view.context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
(52 * context.resources.displayMetrics.density).toInt(),
|
||||
).apply {
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
gravity = Gravity.BOTTOM
|
||||
}
|
||||
setOnClickListener {
|
||||
openToolbox()
|
||||
}
|
||||
textSize = 21f
|
||||
text = "\uD83E\uDDF0"
|
||||
})
|
||||
})
|
||||
}
|
||||
setPadding(25, 0, 25, 0)
|
||||
|
||||
context.scriptRuntime.eachModule {
|
||||
val interfaceManager = getBinding(InterfaceManager::class)?.takeIf {
|
||||
it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX)
|
||||
} ?: return@eachModule
|
||||
addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId ->
|
||||
ScriptInterface(remember {
|
||||
interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf(
|
||||
"alertDialog" to alertDialog,
|
||||
"conversationId" to conversationId,
|
||||
))
|
||||
} ?: return@addComposable)
|
||||
addView(TextView(event.view.context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
setOnClickListener {
|
||||
openToolbox()
|
||||
}
|
||||
textSize = 21f
|
||||
text = "\uD83E\uDDF0"
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
context.scriptRuntime.eachModule {
|
||||
val interfaceManager = getBinding(InterfaceManager::class)?.takeIf {
|
||||
it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX)
|
||||
} ?: return@eachModule
|
||||
addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId ->
|
||||
ScriptInterface(remember {
|
||||
interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf(
|
||||
"alertDialog" to alertDialog,
|
||||
"conversationId" to conversationId,
|
||||
))
|
||||
} ?: return@addComposable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.ui
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.mapper.impl.StreaksExpirationMapper
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class CustomStreaksExpirationFormat: Feature("CustomStreaksExpirationFormat") {
|
||||
private fun Long.padZero(): String {
|
||||
return this.toString().padStart(2, '0')
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
val expirationFormat by context.config.experimental.customStreaksExpirationFormat
|
||||
if (expirationFormat.isNotEmpty() || context.config.userInterface.streakExpirationInfo.get()) {
|
||||
context.mappings.useMapper(StreaksExpirationMapper::class) {
|
||||
runCatching {
|
||||
simpleStreaksFormatterClass.getAsClass()?.hook(formatSimpleStreaksTextMethod.get() ?: return@useMapper, HookStage.BEFORE) { param ->
|
||||
param.setResult(null)
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
val expirationFormat by context.config.experimental.customStreaksExpirationFormat
|
||||
if (expirationFormat.isNotEmpty() || context.config.userInterface.streakExpirationInfo.get()) {
|
||||
context.mappings.useMapper(StreaksExpirationMapper::class) {
|
||||
runCatching {
|
||||
simpleStreaksFormatterClass.getAsClass()?.hook(formatSimpleStreaksTextMethod.get() ?: return@useMapper, HookStage.BEFORE) { param ->
|
||||
param.setResult(null)
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.warn("Failed to hook simpleStreaksFormatterClass : " + it.message)
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.warn("Failed to hook simpleStreaksFormatterClass : " + it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expirationFormat.isEmpty()) return
|
||||
if (expirationFormat.isEmpty()) return@onNextActivityCreate
|
||||
|
||||
context.mappings.useMapper(StreaksExpirationMapper::class) {
|
||||
streaksFormatterClass.getAsClass()?.hook(formatStreaksTextMethod.get() ?: return@useMapper, HookStage.AFTER) { param ->
|
||||
val streaksCount = param.argNullable(2) ?: 0
|
||||
val streaksExpiration = param.argNullable<Any>(3) ?: return@hook
|
||||
context.mappings.useMapper(StreaksExpirationMapper::class) {
|
||||
streaksFormatterClass.getAsClass()?.hook(formatStreaksTextMethod.get() ?: return@useMapper, HookStage.AFTER) { param ->
|
||||
val streaksCount = param.argNullable(2) ?: 0
|
||||
val streaksExpiration = param.argNullable<Any>(3) ?: return@hook
|
||||
|
||||
val hourGlassTimeRemaining = streaksExpiration.getObjectField(hourGlassTimeRemainingField.get() ?: return@hook) as? Long ?: return@hook
|
||||
val expirationTime = streaksExpiration.getObjectField(expirationTimeField.get() ?: return@hook) as? Long ?: return@hook
|
||||
val delta = (expirationTime - System.currentTimeMillis()).milliseconds
|
||||
val hourGlassTimeRemaining = streaksExpiration.getObjectField(hourGlassTimeRemainingField.get() ?: return@hook) as? Long ?: return@hook
|
||||
val expirationTime = streaksExpiration.getObjectField(expirationTimeField.get() ?: return@hook) as? Long ?: return@hook
|
||||
val delta = (expirationTime - System.currentTimeMillis()).milliseconds
|
||||
|
||||
val hourGlassEmoji = if (delta.inWholeMilliseconds in 1..hourGlassTimeRemaining) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else ""
|
||||
val hourGlassEmoji = if (delta.inWholeMilliseconds in 1..hourGlassTimeRemaining) if (expirationTime % 2 == 0L) "\u23F3" else "\u231B" else ""
|
||||
|
||||
param.setResult(expirationFormat
|
||||
.replace("%c", streaksCount.toString())
|
||||
.replace("%e", hourGlassEmoji)
|
||||
.replace("%d", delta.inWholeDays.toString())
|
||||
.replace("%h", (delta.inWholeHours % 24).padZero())
|
||||
.replace("%m", (delta.inWholeMinutes % 60).padZero())
|
||||
.replace("%s", (delta.inWholeSeconds % 60).padZero())
|
||||
.replace("%w", delta.toString())
|
||||
)
|
||||
param.setResult(expirationFormat
|
||||
.replace("%c", streaksCount.toString())
|
||||
.replace("%e", hourGlassEmoji)
|
||||
.replace("%d", delta.inWholeDays.toString())
|
||||
.replace("%h", (delta.inWholeHours % 24).padZero())
|
||||
.replace("%m", (delta.inWholeMinutes % 60).padZero())
|
||||
.replace("%s", (delta.inWholeSeconds % 60).padZero())
|
||||
.replace("%w", delta.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,17 @@ import android.util.TypedValue
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.Hooker
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
||||
|
||||
class CustomizeUI: Feature("Customize UI", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class CustomizeUI: Feature("Customize UI") {
|
||||
private fun getAttribute(name: String): Int {
|
||||
return context.resources.getIdentifier(name, "attr")
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
val customizeUIConfig = context.config.userInterface.customizeUi
|
||||
val themePicker = customizeUIConfig.themePicker.getNullable() ?: return
|
||||
val colorsConfig = context.config.userInterface.customizeUi.colors
|
||||
|
||||
@@ -2,17 +2,18 @@ package me.rhunk.snapenhance.core.features.impl.ui
|
||||
|
||||
import android.view.KeyEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
|
||||
class DefaultVolumeControls : Feature("Default Volume Controls", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class DefaultVolumeControls : Feature("Default Volume Controls") {
|
||||
override fun init() {
|
||||
if (!context.config.global.defaultVolumeControls.get()) return
|
||||
context.mainActivity!!::class.java.hook("onKeyDown", HookStage.BEFORE) { param ->
|
||||
val keyCode = param.arg<Int>(0)
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
param.setResult(false)
|
||||
onNextActivityCreate { activity ->
|
||||
activity::class.java.hook("onKeyDown", HookStage.BEFORE) { param ->
|
||||
val keyCode = param.arg<Int>(0)
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
param.setResult(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,59 +4,60 @@ import android.view.View
|
||||
import android.widget.TextView
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.children
|
||||
import me.rhunk.snapenhance.core.ui.triggerRootCloseTouchEvent
|
||||
import me.rhunk.snapenhance.core.util.ktx.getId
|
||||
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class DisableConfirmationDialogs : Feature("Disable Confirmation Dialogs", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
val disableConfirmationDialogs = context.config.global.disableConfirmationDialogs.get().takeIf { it.isNotEmpty() } ?: return
|
||||
val dialogContent = context.resources.getId("dialog_content")
|
||||
val alertDialogTitle = context.resources.getId("alert_dialog_title")
|
||||
class DisableConfirmationDialogs : Feature("Disable Confirmation Dialogs") {
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
val disableConfirmationDialogs = context.config.global.disableConfirmationDialogs.get().takeIf { it.isNotEmpty() } ?: return@onNextActivityCreate
|
||||
val dialogContent = context.resources.getId("dialog_content")
|
||||
val alertDialogTitle = context.resources.getId("alert_dialog_title")
|
||||
|
||||
val questions = listOf(
|
||||
"erase_message" to "erase_learn_more_dialog_title",
|
||||
"erase_message" to "erase_dialog_title",
|
||||
"erase_message" to "snap_erase_dialog_title",
|
||||
"erase_message" to "snap_erase_learn_more_dialog_title",
|
||||
"remove_friend" to "action_menu_remove_friend_question",
|
||||
"block_friend" to "action_menu_block_friend_question",
|
||||
"ignore_friend" to "action_menu_ignore_friend_question",
|
||||
"hide_friend" to "action_menu_hide_friend_question",
|
||||
"hide_conversation" to "hide_or_block_clear_conversation_dialog_title",
|
||||
"clear_conversation" to "action_menu_clear_conversation_dialog_title"
|
||||
).map { pair ->
|
||||
pair.first to runCatching {
|
||||
Pattern.compile(
|
||||
context.resources.getString(context.resources.getIdentifier(pair.second, "string"))
|
||||
.split("%s").joinToString(".*") {
|
||||
Pattern.quote(it)
|
||||
}, Pattern.CASE_INSENSITIVE)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to compile regex for ${pair.second}", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
val questions = listOf(
|
||||
"erase_message" to "erase_learn_more_dialog_title",
|
||||
"erase_message" to "erase_dialog_title",
|
||||
"erase_message" to "snap_erase_dialog_title",
|
||||
"erase_message" to "snap_erase_learn_more_dialog_title",
|
||||
"remove_friend" to "action_menu_remove_friend_question",
|
||||
"block_friend" to "action_menu_block_friend_question",
|
||||
"ignore_friend" to "action_menu_ignore_friend_question",
|
||||
"hide_friend" to "action_menu_hide_friend_question",
|
||||
"hide_conversation" to "hide_or_block_clear_conversation_dialog_title",
|
||||
"clear_conversation" to "action_menu_clear_conversation_dialog_title"
|
||||
).map { pair ->
|
||||
pair.first to runCatching {
|
||||
Pattern.compile(
|
||||
context.resources.getString(context.resources.getIdentifier(pair.second, "string"))
|
||||
.split("%s").joinToString(".*") {
|
||||
Pattern.quote(it)
|
||||
}, Pattern.CASE_INSENSITIVE)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to compile regex for ${pair.second}", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.parent.id != dialogContent || !event.view::class.java.name.endsWith("SnapButtonView")) return@subscribe
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
if (event.parent.id != dialogContent || !event.view::class.java.name.endsWith("SnapButtonView")) return@subscribe
|
||||
|
||||
val dialogTitle = event.parent.findViewById<TextView>(alertDialogTitle)?.text?.toString() ?: return@subscribe
|
||||
if (event.parent.children().count { it::class.java.name.endsWith("SnapButtonView") } != 0) return@subscribe
|
||||
val dialogTitle = event.parent.findViewById<TextView>(alertDialogTitle)?.text?.toString() ?: return@subscribe
|
||||
if (event.parent.children().count { it::class.java.name.endsWith("SnapButtonView") } != 0) return@subscribe
|
||||
|
||||
questions.forEach { (key, value) ->
|
||||
if (!disableConfirmationDialogs.contains(key)) return@forEach
|
||||
questions.forEach { (key, value) ->
|
||||
if (!disableConfirmationDialogs.contains(key)) return@forEach
|
||||
|
||||
if (value?.matcher(dialogTitle)?.matches() == true) {
|
||||
event.parent.visibility = View.INVISIBLE
|
||||
event.parent.post {
|
||||
event.view.callOnClick()
|
||||
if (value?.matcher(dialogTitle)?.matches() == true) {
|
||||
event.parent.visibility = View.INVISIBLE
|
||||
event.parent.post {
|
||||
event.view.callOnClick()
|
||||
}
|
||||
event.parent.postDelayed({
|
||||
context.mainActivity!!.triggerRootCloseTouchEvent()
|
||||
}, 400)
|
||||
}
|
||||
event.parent.postDelayed({
|
||||
context.mainActivity!!.triggerRootCloseTouchEvent()
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,30 +5,31 @@ import android.text.InputType
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
|
||||
class EditTextOverride : Feature("Edit Text Override", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class EditTextOverride : Feature("Edit Text Override") {
|
||||
override fun init() {
|
||||
val editTextOverride by context.config.userInterface.editTextOverride
|
||||
if (editTextOverride.isEmpty()) return
|
||||
|
||||
if (editTextOverride.contains("bypass_text_input_limit")) {
|
||||
TextView::class.java.getMethod("setFilters", Array<InputFilter>::class.java)
|
||||
.hook(HookStage.BEFORE) { param ->
|
||||
param.setArg(0, param.arg<Array<InputFilter>>(0).filter {
|
||||
it !is InputFilter.LengthFilter
|
||||
}.toTypedArray())
|
||||
}
|
||||
}
|
||||
onNextActivityCreate {
|
||||
if (editTextOverride.contains("bypass_text_input_limit")) {
|
||||
TextView::class.java.getMethod("setFilters", Array<InputFilter>::class.java)
|
||||
.hook(HookStage.BEFORE) { param ->
|
||||
param.setArg(0, param.arg<Array<InputFilter>>(0).filter {
|
||||
it !is InputFilter.LengthFilter
|
||||
}.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
if (editTextOverride.contains("multi_line_chat_input")) {
|
||||
findClass("com.snap.messaging.chat.features.input.InputBarEditText").apply {
|
||||
hookConstructor(HookStage.AFTER) { param ->
|
||||
val editText = param.thisObject<EditText>()
|
||||
editText.inputType = editText.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
if (editTextOverride.contains("multi_line_chat_input")) {
|
||||
findClass("com.snap.messaging.chat.features.input.InputBarEditText").apply {
|
||||
hookConstructor(HookStage.AFTER) { param ->
|
||||
val editText = param.thisObject<EditText>()
|
||||
editText.inputType = editText.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.graphics.drawable.shapes.Shape
|
||||
import android.text.TextPaint
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.res.use
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -17,7 +18,6 @@ import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.experiments.EndToEndEncryption
|
||||
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
||||
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
||||
@@ -29,7 +29,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.getMessageText
|
||||
import java.util.WeakHashMap
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview") {
|
||||
private val endToEndEncryption by lazy { context.feature(EndToEndEncryption::class) }
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val coroutineDispatcher = Dispatchers.IO.limitedParallelism(1)
|
||||
@@ -39,7 +39,7 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams
|
||||
private val sigColorTextPrimary by lazy {
|
||||
context.mainActivity!!.theme.obtainStyledAttributes(
|
||||
intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
|
||||
).getColor(0, 0)
|
||||
).use { it.getColor(0, 0) }
|
||||
}
|
||||
|
||||
private val cachedLayouts = WeakHashMap<String, View>()
|
||||
@@ -73,82 +73,84 @@ class FriendFeedMessagePreview : Feature("FriendFeedMessagePreview", loadParams
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (setting.globalState != true) return
|
||||
|
||||
val ffItemId = context.resources.getId("ff_item")
|
||||
onNextActivityCreate {
|
||||
val ffItemId = context.resources.getId("ff_item")
|
||||
|
||||
val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat()
|
||||
val ffSdlAvatarMargin = context.resources.getDimens("ff_sdl_avatar_margin")
|
||||
val ffSdlAvatarSize = context.resources.getDimens("ff_sdl_avatar_size")
|
||||
val ffSdlPrimaryTextStartMargin = context.resources.getDimens("ff_sdl_primary_text_start_margin").toFloat()
|
||||
val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat()
|
||||
val ffSdlAvatarMargin = context.resources.getDimens("ff_sdl_avatar_margin")
|
||||
val ffSdlAvatarSize = context.resources.getDimens("ff_sdl_avatar_size")
|
||||
val ffSdlPrimaryTextStartMargin = context.resources.getDimens("ff_sdl_primary_text_start_margin").toFloat()
|
||||
|
||||
val feedEntryHeight = ffSdlAvatarSize + ffSdlAvatarMargin * 2 + (4 * context.resources.displayMetrics.density).toInt()
|
||||
val separatorHeight = (context.resources.displayMetrics.density * 2).toInt()
|
||||
val avenirNextMedium = context.resources.getFont(context.resources.getIdentifier("avenir_next_medium", "font"))
|
||||
val textPaint = TextPaint().apply {
|
||||
textSize = secondaryTextSize
|
||||
typeface = avenirNextMedium
|
||||
}
|
||||
val feedEntryHeight = ffSdlAvatarSize + ffSdlAvatarMargin * 2 + (4 * context.resources.displayMetrics.density).toInt()
|
||||
val separatorHeight = (context.resources.displayMetrics.density * 2).toInt()
|
||||
val avenirNextMedium = context.resources.getFont(context.resources.getIdentifier("avenir_next_medium", "font"))
|
||||
val textPaint = TextPaint().apply {
|
||||
textSize = secondaryTextSize
|
||||
typeface = avenirNextMedium
|
||||
}
|
||||
|
||||
context.event.subscribe(BuildMessageEvent::class) { param ->
|
||||
val conversationId = param.message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
|
||||
val cachedView = cachedLayouts[conversationId] ?: return@subscribe
|
||||
context.coroutineScope.launch {
|
||||
fetchMessages(conversationId) {
|
||||
cachedView.postInvalidateDelayed(100L)
|
||||
context.event.subscribe(BuildMessageEvent::class) { param ->
|
||||
val conversationId = param.message.messageDescriptor?.conversationId?.toString() ?: return@subscribe
|
||||
val cachedView = cachedLayouts[conversationId] ?: return@subscribe
|
||||
context.coroutineScope.launch {
|
||||
fetchMessages(conversationId) {
|
||||
cachedView.postInvalidateDelayed(100L)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { param ->
|
||||
param.friendFeedItem { conversationId ->
|
||||
val frameLayout = param.view as ViewGroup
|
||||
val ffItem = frameLayout.findViewById<View>(ffItemId)
|
||||
context.event.subscribe(BindViewEvent::class) { param ->
|
||||
param.friendFeedItem { conversationId ->
|
||||
val frameLayout = param.view as ViewGroup
|
||||
val ffItem = frameLayout.findViewById<View>(ffItemId)
|
||||
|
||||
context.coroutineScope.launch(coroutineDispatcher) {
|
||||
withContext(Dispatchers.Main) {
|
||||
cachedLayouts.remove(conversationId)
|
||||
frameLayout.removeForegroundDrawable("ffItem")
|
||||
}
|
||||
|
||||
fetchMessages(conversationId) {
|
||||
var maxTextHeight = 0
|
||||
val previewContainerHeight = messageCache[conversationId]?.sumOf { msg ->
|
||||
val rect = Rect()
|
||||
textPaint.getTextBounds(msg, 0, msg.length, rect)
|
||||
rect.height().also {
|
||||
if (it > maxTextHeight) maxTextHeight = it
|
||||
}.plus(separatorHeight)
|
||||
} ?: run {
|
||||
ffItem.layoutParams = ffItem.layoutParams.apply {
|
||||
height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
return@fetchMessages
|
||||
context.coroutineScope.launch(coroutineDispatcher) {
|
||||
withContext(Dispatchers.Main) {
|
||||
cachedLayouts.remove(conversationId)
|
||||
frameLayout.removeForegroundDrawable("ffItem")
|
||||
}
|
||||
|
||||
ffItem.layoutParams = ffItem.layoutParams.apply {
|
||||
height = feedEntryHeight + previewContainerHeight + separatorHeight
|
||||
}
|
||||
|
||||
cachedLayouts[conversationId] = frameLayout
|
||||
|
||||
frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
val offsetY = canvas.height.toFloat() - previewContainerHeight
|
||||
paint.textSize = secondaryTextSize
|
||||
paint.color = sigColorTextPrimary
|
||||
paint.typeface = avenirNextMedium
|
||||
|
||||
messageCache[conversationId]?.forEachIndexed { index, messageString ->
|
||||
canvas.drawText(messageString,
|
||||
feedEntryHeight + ffSdlPrimaryTextStartMargin,
|
||||
offsetY + index * maxTextHeight,
|
||||
paint
|
||||
)
|
||||
fetchMessages(conversationId) {
|
||||
var maxTextHeight = 0
|
||||
val previewContainerHeight = messageCache[conversationId]?.sumOf { msg ->
|
||||
val rect = Rect()
|
||||
textPaint.getTextBounds(msg, 0, msg.length, rect)
|
||||
rect.height().also {
|
||||
if (it > maxTextHeight) maxTextHeight = it
|
||||
}.plus(separatorHeight)
|
||||
} ?: run {
|
||||
ffItem.layoutParams = ffItem.layoutParams.apply {
|
||||
height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
return@fetchMessages
|
||||
}
|
||||
}))
|
||||
|
||||
ffItem.layoutParams = ffItem.layoutParams.apply {
|
||||
height = feedEntryHeight + previewContainerHeight + separatorHeight
|
||||
}
|
||||
|
||||
cachedLayouts[conversationId] = frameLayout
|
||||
|
||||
frameLayout.addForegroundDrawable("ffItem", ShapeDrawable(object: Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
val offsetY = canvas.height.toFloat() - previewContainerHeight
|
||||
paint.textSize = secondaryTextSize
|
||||
paint.color = sigColorTextPrimary
|
||||
paint.typeface = avenirNextMedium
|
||||
|
||||
messageCache[conversationId]?.forEachIndexed { index, messageString ->
|
||||
canvas.drawText(messageString,
|
||||
feedEntryHeight + ffSdlPrimaryTextStartMargin,
|
||||
offsetY + index * maxTextHeight,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package me.rhunk.snapenhance.core.features.impl.ui
|
||||
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.common.data.RuleState
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
@@ -11,7 +11,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
|
||||
|
||||
class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED, loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED) {
|
||||
private fun createDeletedFeedEntry(conversationIdInstance: Any) = findClass("com.snapchat.client.messaging.DeletedFeedEntry").dataBuilder {
|
||||
from("mFeedEntryIdentifier") {
|
||||
set("mConversationId", conversationIdInstance)
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.ui
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.hookConstructor
|
||||
import me.rhunk.snapenhance.core.util.ktx.setObjectField
|
||||
import me.rhunk.snapenhance.mapper.impl.FriendingDataSourcesMapper
|
||||
|
||||
class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class HideQuickAddFriendFeed : Feature("HideQuickAddFriendFeed") {
|
||||
override fun init() {
|
||||
if (!context.config.userInterface.hideQuickAddFriendFeed.get()) return
|
||||
|
||||
context.mappings.useMapper(FriendingDataSourcesMapper::class) {
|
||||
classReference.getAsClass()?.hookConstructor(HookStage.AFTER) { param ->
|
||||
param.thisObject<Any>().setObjectField(
|
||||
quickAddSourceListField.get()!!,
|
||||
arrayListOf<Any>()
|
||||
)
|
||||
onNextActivityCreate {
|
||||
context.mappings.useMapper(FriendingDataSourcesMapper::class) {
|
||||
classReference.getAsClass()?.hookConstructor(HookStage.AFTER) { param ->
|
||||
param.thisObject<Any>().setObjectField(
|
||||
quickAddSourceListField.get()!!,
|
||||
arrayListOf<Any>()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package me.rhunk.snapenhance.core.features.impl.ui
|
||||
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import me.rhunk.snapenhance.core.util.dataBuilder
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
@@ -11,7 +10,7 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull
|
||||
import me.rhunk.snapenhance.core.util.ktx.setObjectField
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
|
||||
class HideStreakRestore : Feature("HideStreakRestore", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class HideStreakRestore : Feature("HideStreakRestore") {
|
||||
override fun init() {
|
||||
if (!context.config.userInterface.hideStreakRestore.get()) return
|
||||
|
||||
|
||||
@@ -26,119 +26,120 @@ import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.AppleLogo
|
||||
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
||||
import kotlin.random.Random
|
||||
|
||||
class MessageIndicators : Feature("Message Indicators", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class MessageIndicators : Feature("Message Indicators") {
|
||||
override fun init() {
|
||||
val messageIndicatorsConfig = context.config.userInterface.messageIndicators.getNullable() ?: return
|
||||
if (messageIndicatorsConfig.isEmpty()) return
|
||||
|
||||
val messageInfoTag = Random.nextLong().toString()
|
||||
val appleLogo = AppleLogo
|
||||
onNextActivityCreate {
|
||||
val appleLogo = AppleLogo
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { _, messageId ->
|
||||
val parentLinearLayout = event.view.parent as? ViewGroup ?: return@subscribe
|
||||
parentLinearLayout.findViewWithTag<View>(messageInfoTag)?.let { parentLinearLayout.removeView(it) }
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { _, messageId ->
|
||||
val parentLinearLayout = event.view.parent as? ViewGroup ?: return@subscribe
|
||||
parentLinearLayout.findViewWithTag<View>(messageInfoTag)?.let { parentLinearLayout.removeView(it) }
|
||||
|
||||
event.view.removeForegroundDrawable("messageIndicators")
|
||||
event.view.removeForegroundDrawable("messageIndicators")
|
||||
|
||||
val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage
|
||||
if (message.contentType != ContentType.SNAP.id && message.contentType != ContentType.EXTERNAL_MEDIA.id) return@chatMessage
|
||||
val reader = ProtoReader(message.messageContent ?: return@chatMessage)
|
||||
val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage
|
||||
if (message.contentType != ContentType.SNAP.id && message.contentType != ContentType.EXTERNAL_MEDIA.id) return@chatMessage
|
||||
val reader = ProtoReader(message.messageContent ?: return@chatMessage)
|
||||
|
||||
createComposeView(event.view.context) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.padding(top = 4.dp, end = 1.dp),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val hasEncryption by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.getByteArray(4, 3, 3) != null || reader.containsPath(3, 99, 3)
|
||||
}
|
||||
val sentFromIosDevice by rememberAsyncMutableState(defaultValue = false) {
|
||||
if (reader.containsPath(4, 4, 3)) !reader.containsPath(4, 4, 3, 3, 17) else reader.getVarInt(4, 4, 11, 17, 7) != null
|
||||
}
|
||||
val sentFromWebApp by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.getVarInt(4, 4, *(if (reader.containsPath(4, 4, 3)) intArrayOf(3, 3, 22, 1) else intArrayOf(11, 22, 1))) == 7L
|
||||
}
|
||||
val sentWithLocation by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.getVarInt(4, 4, 11, 17, 5) != null
|
||||
}
|
||||
val sentUsingOvfEditor by rememberAsyncMutableState(defaultValue = false) {
|
||||
(reader.getString(4, 4, 11, 12, 1) ?: reader.getString(4, 4, 11, 13, 4, 1, 2, 12, 20, 1)) == "c13129f7-fe4a-44c4-9b9d-e0b26fee8f82"
|
||||
}
|
||||
val sentUsingDirectorMode by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.followPath(4, 4, 11, 28)?.let {
|
||||
(it.getVarInt(1) to it.getVarInt(2)) == (0L to 0L)
|
||||
} == true || reader.getByteArray(4, 4, 11, 13, 4, 1, 2, 12, 27, 1) != null
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
createComposeView(event.view.context) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.padding(top = 4.dp, end = 1.dp),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
if (sentWithLocation && messageIndicatorsConfig.contains("location_indicator")) {
|
||||
Image(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
colorFilter = ColorFilter.tint(Color.Green),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
val hasEncryption by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.getByteArray(4, 3, 3) != null || reader.containsPath(3, 99, 3)
|
||||
}
|
||||
if (messageIndicatorsConfig.contains("platform_indicator")) {
|
||||
Image(
|
||||
imageVector = when {
|
||||
sentFromWebApp -> Icons.Default.Laptop
|
||||
sentFromIosDevice -> appleLogo
|
||||
else -> Icons.Default.Android
|
||||
},
|
||||
colorFilter = ColorFilter.tint(Color.Green),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
val sentFromIosDevice by rememberAsyncMutableState(defaultValue = false) {
|
||||
if (reader.containsPath(4, 4, 3)) !reader.containsPath(4, 4, 3, 3, 17) else reader.getVarInt(4, 4, 11, 17, 7) != null
|
||||
}
|
||||
if (hasEncryption && messageIndicatorsConfig.contains("encryption_indicator")) {
|
||||
Image(
|
||||
imageVector = Icons.Default.Lock,
|
||||
colorFilter = ColorFilter.tint(Color.Green),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
val sentFromWebApp by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.getVarInt(4, 4, *(if (reader.containsPath(4, 4, 3)) intArrayOf(3, 3, 22, 1) else intArrayOf(11, 22, 1))) == 7L
|
||||
}
|
||||
if (sentUsingDirectorMode && messageIndicatorsConfig.contains("director_mode_indicator")) {
|
||||
Image(
|
||||
imageVector = Icons.Default.Edit,
|
||||
colorFilter = ColorFilter.tint(Color.Red),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
val sentWithLocation by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.getVarInt(4, 4, 11, 17, 5) != null
|
||||
}
|
||||
if (sentUsingOvfEditor && messageIndicatorsConfig.contains("ovf_editor_indicator")) {
|
||||
Text(
|
||||
text = "OVF",
|
||||
color = Color.Red,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
val sentUsingOvfEditor by rememberAsyncMutableState(defaultValue = false) {
|
||||
(reader.getString(4, 4, 11, 12, 1) ?: reader.getString(4, 4, 11, 13, 4, 1, 2, 12, 20, 1)) == "c13129f7-fe4a-44c4-9b9d-e0b26fee8f82"
|
||||
}
|
||||
val sentUsingDirectorMode by rememberAsyncMutableState(defaultValue = false) {
|
||||
reader.followPath(4, 4, 11, 28)?.let {
|
||||
(it.getVarInt(1) to it.getVarInt(2)) == (0L to 0L)
|
||||
} == true || reader.getByteArray(4, 4, 11, 13, 4, 1, 2, 12, 27, 1) != null
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (sentWithLocation && messageIndicatorsConfig.contains("location_indicator")) {
|
||||
Image(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
colorFilter = ColorFilter.tint(Color.Green),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
}
|
||||
if (messageIndicatorsConfig.contains("platform_indicator")) {
|
||||
Image(
|
||||
imageVector = when {
|
||||
sentFromWebApp -> Icons.Default.Laptop
|
||||
sentFromIosDevice -> appleLogo
|
||||
else -> Icons.Default.Android
|
||||
},
|
||||
colorFilter = ColorFilter.tint(Color.Green),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
}
|
||||
if (hasEncryption && messageIndicatorsConfig.contains("encryption_indicator")) {
|
||||
Image(
|
||||
imageVector = Icons.Default.Lock,
|
||||
colorFilter = ColorFilter.tint(Color.Green),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
}
|
||||
if (sentUsingDirectorMode && messageIndicatorsConfig.contains("director_mode_indicator")) {
|
||||
Image(
|
||||
imageVector = Icons.Default.Edit,
|
||||
colorFilter = ColorFilter.tint(Color.Red),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
}
|
||||
if (sentUsingOvfEditor && messageIndicatorsConfig.contains("ovf_editor_indicator")) {
|
||||
Text(
|
||||
text = "OVF",
|
||||
color = Color.Red,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 10.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
tag = messageInfoTag
|
||||
addOnLayoutChangeListener { _, left, _, right, _, _, _, _, _ ->
|
||||
layout(left, 0, right, 0)
|
||||
}
|
||||
setPadding(0, 0, 0, -(50 * event.view.resources.displayMetrics.density).toInt())
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
parentLinearLayout.addView(this)
|
||||
}
|
||||
}.apply {
|
||||
tag = messageInfoTag
|
||||
addOnLayoutChangeListener { _, left, _, right, _, _, _, _, _ ->
|
||||
layout(left, 0, right, 0)
|
||||
}
|
||||
setPadding(0, 0, 0, -(50 * event.view.resources.displayMetrics.density).toInt())
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
parentLinearLayout.addView(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import android.net.Uri
|
||||
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
|
||||
import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
|
||||
class OldBitmojiSelfie : Feature("OldBitmojiSelfie", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||
class OldBitmojiSelfie : Feature("OldBitmojiSelfie") {
|
||||
override fun init() {
|
||||
val urlPrefixes = arrayOf("https://images.bitmoji.com/3d/render/", "https://cf-st.sc-cdn.net/3d/render/")
|
||||
val oldBitmojiSelfie = context.config.userInterface.oldBitmojiSelfie.getNullable() ?: return
|
||||
|
||||
@@ -2,7 +2,6 @@ package me.rhunk.snapenhance.core.features.impl.ui
|
||||
|
||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||
import me.rhunk.snapenhance.common.data.RuleState
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.Hooker
|
||||
@@ -12,8 +11,8 @@ import me.rhunk.snapenhance.core.util.ktx.getObjectField
|
||||
import me.rhunk.snapenhance.core.util.ktx.setObjectField
|
||||
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
|
||||
|
||||
class PinConversations : MessagingRuleFeature("PinConversations", MessagingRuleType.PIN_CONVERSATION, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
override fun onActivityCreate() {
|
||||
class PinConversations : MessagingRuleFeature("PinConversations", MessagingRuleType.PIN_CONVERSATION) {
|
||||
override fun init() {
|
||||
if (!context.config.messaging.unlimitedConversationPinning.get()) return
|
||||
|
||||
context.classCache.feedManager.hook("setPinnedConversationStatus", HookStage.BEFORE) { param ->
|
||||
|
||||
@@ -10,7 +10,6 @@ import me.rhunk.snapenhance.common.data.ContentType
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
||||
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
||||
import me.rhunk.snapenhance.core.util.EvictingMap
|
||||
@@ -22,7 +21,7 @@ import me.rhunk.snapenhance.core.util.media.PreviewUtils
|
||||
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
|
||||
import java.io.File
|
||||
|
||||
class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_SYNC or FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class SnapPreview : Feature("SnapPreview") {
|
||||
private val mediaFileCache = EvictingMap<String, File>(500) // mMediaId => mediaFile
|
||||
private val bitmapCache = EvictingMap<String, Bitmap>(50) // filePath => bitmap
|
||||
|
||||
@@ -44,48 +43,46 @@ class SnapPreview : Feature("SnapPreview", loadParams = FeatureLoadParams.INIT_S
|
||||
mediaFileCache[mediaId.substringAfter("-")] = File(filePath.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
override fun onActivityCreate() {
|
||||
if (!isEnabled) return
|
||||
val chatMediaCardHeight = context.resources.getDimens("chat_media_card_height")
|
||||
val chatMediaCardSnapMargin = context.resources.getDimens("chat_media_card_snap_margin")
|
||||
val chatMediaCardSnapMarginStartSdl = context.resources.getDimens("chat_media_card_snap_margin_start_sdl")
|
||||
onNextActivityCreate {
|
||||
val chatMediaCardHeight = context.resources.getDimens("chat_media_card_height")
|
||||
val chatMediaCardSnapMargin = context.resources.getDimens("chat_media_card_snap_margin")
|
||||
val chatMediaCardSnapMarginStartSdl = context.resources.getDimens("chat_media_card_snap_margin_start_sdl")
|
||||
|
||||
fun decodeMedia(file: File) = runCatching {
|
||||
bitmapCache.getOrPut(file.absolutePath) {
|
||||
PreviewUtils.resizeBitmap(
|
||||
PreviewUtils.createPreviewFromFile(file) ?: return@runCatching null,
|
||||
chatMediaCardHeight - chatMediaCardSnapMargin,
|
||||
chatMediaCardHeight - chatMediaCardSnapMargin
|
||||
)
|
||||
}
|
||||
}.getOrNull()
|
||||
fun decodeMedia(file: File) = runCatching {
|
||||
bitmapCache.getOrPut(file.absolutePath) {
|
||||
PreviewUtils.resizeBitmap(
|
||||
PreviewUtils.createPreviewFromFile(file) ?: return@runCatching null,
|
||||
chatMediaCardHeight - chatMediaCardSnapMargin,
|
||||
chatMediaCardHeight - chatMediaCardSnapMargin
|
||||
)
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { _, messageId ->
|
||||
event.view.removeForegroundDrawable("snapPreview")
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
event.chatMessage { _, messageId ->
|
||||
event.view.removeForegroundDrawable("snapPreview")
|
||||
|
||||
val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage
|
||||
val messageReader = ProtoReader(message.messageContent ?: return@chatMessage)
|
||||
val contentType = ContentType.fromMessageContainer(messageReader.followPath(4, 4))
|
||||
val message = context.database.getConversationMessageFromId(messageId.toLong()) ?: return@chatMessage
|
||||
val messageReader = ProtoReader(message.messageContent ?: return@chatMessage)
|
||||
val contentType = ContentType.fromMessageContainer(messageReader.followPath(4, 4))
|
||||
|
||||
if (contentType != ContentType.SNAP || message.isSaved == 1) return@chatMessage
|
||||
if (contentType != ContentType.SNAP || message.isSaved == 1) return@chatMessage
|
||||
|
||||
val mediaIdKey = messageReader.getString(4, 5, 1, 3, 2, 2) ?: return@chatMessage
|
||||
val mediaIdKey = messageReader.getString(4, 5, 1, 3, 2, 2) ?: return@chatMessage
|
||||
|
||||
event.view.addForegroundDrawable("snapPreview", ShapeDrawable(object: Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
val bitmap = mediaFileCache[mediaIdKey]?.let { decodeMedia(it) } ?: return
|
||||
event.view.addForegroundDrawable("snapPreview", ShapeDrawable(object: Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
val bitmap = mediaFileCache[mediaIdKey]?.let { decodeMedia(it) } ?: return
|
||||
|
||||
canvas.drawBitmap(bitmap,
|
||||
canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(),
|
||||
(canvas.height - bitmap.height) / 2f,
|
||||
null
|
||||
)
|
||||
}
|
||||
}))
|
||||
canvas.drawBitmap(bitmap,
|
||||
canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(),
|
||||
(canvas.height - bitmap.height) / 2f,
|
||||
null
|
||||
)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,47 +7,48 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import me.rhunk.snapenhance.core.util.EvictingMap
|
||||
import me.rhunk.snapenhance.core.util.ktx.getId
|
||||
|
||||
class SpotlightCommentsUsername : Feature("SpotlightCommentsUsername", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class SpotlightCommentsUsername : Feature("SpotlightCommentsUsername") {
|
||||
private val usernameCache = EvictingMap<String, String>(150)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (!context.config.global.spotlightCommentsUsername.get()) return
|
||||
|
||||
val messaging = context.feature(Messaging::class)
|
||||
val commentsCreatorBadgeTimestampId = context.resources.getId("comments_creator_badge_timestamp")
|
||||
onNextActivityCreate(defer = true) {
|
||||
val messaging = context.feature(Messaging::class)
|
||||
val commentsCreatorBadgeTimestampId = context.resources.getId("comments_creator_badge_timestamp")
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
val commentsCreatorBadgeTimestamp = event.view.findViewById<TextView>(commentsCreatorBadgeTimestampId) ?: return@subscribe
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
val commentsCreatorBadgeTimestamp = event.view.findViewById<TextView>(commentsCreatorBadgeTimestampId) ?: return@subscribe
|
||||
|
||||
val posterUserId = event.prevModel.toString().takeIf { it.startsWith("Comment") }
|
||||
?.substringAfter("posterUserId=")?.substringBefore(",")?.substringBefore(")") ?: return@subscribe
|
||||
val posterUserId = event.prevModel.toString().takeIf { it.startsWith("Comment") }
|
||||
?.substringAfter("posterUserId=")?.substringBefore(",")?.substringBefore(")") ?: return@subscribe
|
||||
|
||||
fun setUsername(username: String) {
|
||||
usernameCache[posterUserId] = username
|
||||
if (commentsCreatorBadgeTimestamp.text.contains(username)) return
|
||||
commentsCreatorBadgeTimestamp.text = " (${username})" + commentsCreatorBadgeTimestamp.text.toString()
|
||||
}
|
||||
fun setUsername(username: String) {
|
||||
usernameCache[posterUserId] = username
|
||||
if (commentsCreatorBadgeTimestamp.text.contains(username)) return
|
||||
commentsCreatorBadgeTimestamp.text = " (${username})" + commentsCreatorBadgeTimestamp.text.toString()
|
||||
}
|
||||
|
||||
usernameCache[posterUserId]?.let {
|
||||
setUsername(it)
|
||||
return@subscribe
|
||||
}
|
||||
usernameCache[posterUserId]?.let {
|
||||
setUsername(it)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
context.coroutineScope.launch {
|
||||
val username = runCatching {
|
||||
messaging.fetchSnapchatterInfos(listOf(posterUserId)).firstOrNull()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to fetch snapchatter info for user $posterUserId", it)
|
||||
}.getOrNull()?.username ?: return@launch
|
||||
context.coroutineScope.launch {
|
||||
val username = runCatching {
|
||||
messaging.fetchSnapchatterInfos(listOf(posterUserId)).firstOrNull()
|
||||
}.onFailure {
|
||||
context.log.error("Failed to fetch snapchatter info for user $posterUserId", it)
|
||||
}.getOrNull()?.username ?: return@launch
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
setUsername(username)
|
||||
withContext(Dispatchers.Main) {
|
||||
setUsername(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.graphics.drawable.shapes.Shape
|
||||
import androidx.core.content.res.use
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
|
||||
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
||||
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
||||
@@ -17,7 +17,7 @@ import me.rhunk.snapenhance.core.util.EvictingMap
|
||||
import me.rhunk.snapenhance.core.util.ktx.getDimens
|
||||
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
||||
|
||||
class StealthModeIndicator : Feature("StealthModeIndicator", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class StealthModeIndicator : Feature("StealthModeIndicator") {
|
||||
private val stealthMode by lazy { context.feature(StealthMode::class) }
|
||||
private val listeners = EvictingMap<String, (Boolean) -> Unit>(100)
|
||||
|
||||
@@ -30,46 +30,48 @@ class StealthModeIndicator : Feature("StealthModeIndicator", loadParams = Featur
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
override fun init() {
|
||||
if (!context.config.userInterface.stealthModeIndicator.get()) return
|
||||
|
||||
val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat()
|
||||
val sigColorTextPrimary = context.mainActivity!!.obtainStyledAttributes(
|
||||
intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
|
||||
).use { it.getColor(0, 0) }
|
||||
onNextActivityCreate {
|
||||
val secondaryTextSize = context.resources.getDimens("ff_feed_cell_secondary_text_size").toFloat()
|
||||
val sigColorTextPrimary = context.mainActivity!!.obtainStyledAttributes(
|
||||
intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
|
||||
).use { it.getColor(0, 0) }
|
||||
|
||||
stealthMode.addStateListener { conversationId, state ->
|
||||
runCatching {
|
||||
listeners[conversationId]?.invoke(state)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to update stealth mode indicator", it)
|
||||
}
|
||||
}
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
fun updateStealthIndicator(isStealth: Boolean = true) {
|
||||
event.view.removeForegroundDrawable("stealthModeIndicator")
|
||||
if (!isStealth || !event.view.isAttachedToWindow) return
|
||||
event.view.addForegroundDrawable("stealthModeIndicator", ShapeDrawable(object : Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
paint.textSize = secondaryTextSize
|
||||
paint.color = sigColorTextPrimary
|
||||
canvas.drawText(
|
||||
"\uD83D\uDC7B",
|
||||
0f,
|
||||
canvas.height.toFloat() - secondaryTextSize / 2,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
event.friendFeedItem { conversationId ->
|
||||
listeners[conversationId] = addStateListener@{ stealth ->
|
||||
updateStealthIndicator(stealth)
|
||||
stealthMode.addStateListener { conversationId, state ->
|
||||
runCatching {
|
||||
listeners[conversationId]?.invoke(state)
|
||||
}.onFailure {
|
||||
context.log.error("Failed to update stealth mode indicator", it)
|
||||
}
|
||||
fetchStealthState(conversationId) { isStealth ->
|
||||
updateStealthIndicator(isStealth)
|
||||
}
|
||||
|
||||
context.event.subscribe(BindViewEvent::class) { event ->
|
||||
fun updateStealthIndicator(isStealth: Boolean = true) {
|
||||
event.view.removeForegroundDrawable("stealthModeIndicator")
|
||||
if (!isStealth || !event.view.isAttachedToWindow) return
|
||||
event.view.addForegroundDrawable("stealthModeIndicator", ShapeDrawable(object : Shape() {
|
||||
override fun draw(canvas: Canvas, paint: Paint) {
|
||||
paint.textSize = secondaryTextSize
|
||||
paint.color = sigColorTextPrimary
|
||||
canvas.drawText(
|
||||
"\uD83D\uDC7B",
|
||||
0f,
|
||||
canvas.height.toFloat() - secondaryTextSize / 2,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
event.friendFeedItem { conversationId ->
|
||||
listeners[conversationId] = addStateListener@{ stealth ->
|
||||
updateStealthIndicator(stealth)
|
||||
}
|
||||
fetchStealthState(conversationId) { isStealth ->
|
||||
updateStealthIndicator(isStealth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,12 @@ import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
||||
import me.rhunk.snapenhance.core.event.events.impl.LayoutInflateEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.util.hook.HookStage
|
||||
import me.rhunk.snapenhance.core.util.hook.Hooker
|
||||
import me.rhunk.snapenhance.core.util.hook.hook
|
||||
import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
||||
|
||||
class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
|
||||
class UITweaks : Feature("UITweaks") {
|
||||
private val identifierCache = mutableMapOf<String, Int>()
|
||||
|
||||
fun getId(name: String, defType: String): Int {
|
||||
@@ -47,7 +46,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreate() {
|
||||
private fun onActivityCreate() {
|
||||
val blockAds by context.config.global.blockAds
|
||||
val hiddenElements by context.config.userInterface.hideUiComponents
|
||||
val hideStorySuggestions by context.config.userInterface.hideStorySuggestions
|
||||
@@ -172,4 +171,10 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
onNextActivityCreate {
|
||||
onActivityCreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.res.use
|
||||
import me.rhunk.snapenhance.core.SnapEnhance
|
||||
import me.rhunk.snapenhance.core.util.ktx.getDimens
|
||||
import me.rhunk.snapenhance.core.util.ktx.getDimensFloat
|
||||
@@ -136,10 +137,10 @@ object ViewAppearanceHelper {
|
||||
|
||||
val sigColorTextPrimary = component.context.theme.obtainStyledAttributes(
|
||||
intArrayOf(resources.getIdentifier("sigColorTextPrimary", "attr"))
|
||||
).getColor(0, 0)
|
||||
).use { it.getColor(0, 0) }
|
||||
val sigColorBackgroundSurface = component.context.theme.obtainStyledAttributes(
|
||||
intArrayOf(resources.getIdentifier("sigColorBackgroundSurface", "attr"))
|
||||
).getColor(0, 0)
|
||||
).use { it.getColor(0, 0) }
|
||||
|
||||
val actionSheetDefaultCellHeight = resources.getDimens("action_sheet_default_cell_height")
|
||||
val actionSheetCornerRadius = resources.getDimensFloat("action_sheet_corner_radius")
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
|
||||
import me.rhunk.snapenhance.core.features.Feature
|
||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||
import me.rhunk.snapenhance.core.features.impl.COFOverride
|
||||
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
|
||||
import me.rhunk.snapenhance.core.ui.findParent
|
||||
@@ -18,7 +17,7 @@ import me.rhunk.snapenhance.core.util.ktx.getIdentifier
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
|
||||
class MenuViewInjector : Feature("MenuViewInjector") {
|
||||
private val menuMap by lazy {
|
||||
arrayOf(
|
||||
NewChatActionMenu(),
|
||||
@@ -40,113 +39,115 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
|
||||
return menuMap[menuClass] as? T
|
||||
}
|
||||
|
||||
override fun asyncOnActivityCreate() {
|
||||
menuMap.forEach { it.value.init() }
|
||||
override fun init() {
|
||||
onNextActivityCreate(defer = true) {
|
||||
menuMap.forEach { it.value.init() }
|
||||
|
||||
val messaging = context.feature(Messaging::class)
|
||||
val messaging = context.feature(Messaging::class)
|
||||
|
||||
val actionSheetItemsContainerLayoutId = context.resources.getIdentifier("action_sheet_items_container", "id")
|
||||
val actionMenuTitle = context.resources.getIdentifier("action_menu_title", "id")
|
||||
val actionMenu = context.resources.getIdentifier("action_menu", "id")
|
||||
val componentsHolder = context.resources.getIdentifier("components_holder", "id")
|
||||
val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id")
|
||||
val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id")
|
||||
val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id")
|
||||
val actionSheetItemsContainerLayoutId = context.resources.getIdentifier("action_sheet_items_container", "id")
|
||||
val actionMenuTitle = context.resources.getIdentifier("action_menu_title", "id")
|
||||
val actionMenu = context.resources.getIdentifier("action_menu", "id")
|
||||
val componentsHolder = context.resources.getIdentifier("components_holder", "id")
|
||||
val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id")
|
||||
val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id")
|
||||
val chatActionMenu = context.resources.getIdentifier("chat_action_menu", "id")
|
||||
|
||||
val hasV2ActionMenu = { context.feature(COFOverride::class).hasActionMenuV2 }
|
||||
val hasV2ActionMenu = { context.feature(COFOverride::class).hasActionMenuV2 }
|
||||
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
val originalAddView: (View) -> Unit = {
|
||||
event.adapter.invokeOriginal(arrayOf(it, -1,
|
||||
FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
val viewGroup: ViewGroup = event.parent
|
||||
val childView: View = event.view
|
||||
menuMap[OperaContextActionMenu::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
|
||||
if (event.view.id == actionSheetItemsContainerLayoutId) {
|
||||
event.view.post {
|
||||
if (event.parent.findParent(4) {
|
||||
it.findViewById<View>(actionMenuTitle) != null
|
||||
} == null) return@post
|
||||
|
||||
val views = mutableListOf<View>()
|
||||
menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, event.view) {
|
||||
views.add(it)
|
||||
}
|
||||
views.reversed().forEach { (event.view as ViewGroup).addView(it, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
if (childView.id == contextMenuButtonIconView) {
|
||||
menuMap[OperaViewerIcons::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
}
|
||||
|
||||
if (event.parent.id == componentsHolder && childView.id == feedNewChat) {
|
||||
menuMap[SettingsGearInjector::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (viewGroup !is LinearLayout && childView.id == chatActionMenu && context.isDeveloper) {
|
||||
event.view = LinearLayout(childView.context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(
|
||||
(menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).createDebugInfoView(childView.context)
|
||||
)
|
||||
addView(event.view)
|
||||
}
|
||||
}
|
||||
|
||||
if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && hasV2ActionMenu()) {
|
||||
(menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !hasV2ActionMenu()) {
|
||||
if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe
|
||||
menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (viewGroup !is LinearLayout && childView.id == actionMenu && messaging.lastFocusedConversationType == 1) {
|
||||
val injectedLayout = LinearLayout(childView.context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.BOTTOM
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
addView(childView)
|
||||
}
|
||||
|
||||
event.parent.post {
|
||||
injectedLayout.addView(ScrollView(injectedLayout.context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
context.event.subscribe(AddViewEvent::class) { event ->
|
||||
val originalAddView: (View) -> Unit = {
|
||||
event.adapter.invokeOriginal(arrayOf(it, -1,
|
||||
FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
weight = 1f;
|
||||
setMargins(0, 100, 0, 0)
|
||||
}
|
||||
|
||||
addView(LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view ->
|
||||
view.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(0, 5, 0, 5)
|
||||
}
|
||||
addView(view)
|
||||
}
|
||||
})
|
||||
}, 0)
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
event.view = injectedLayout
|
||||
val viewGroup: ViewGroup = event.parent
|
||||
val childView: View = event.view
|
||||
menuMap[OperaContextActionMenu::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
|
||||
if (event.view.id == actionSheetItemsContainerLayoutId) {
|
||||
event.view.post {
|
||||
if (event.parent.findParent(4) {
|
||||
it.findViewById<View>(actionMenuTitle) != null
|
||||
} == null) return@post
|
||||
|
||||
val views = mutableListOf<View>()
|
||||
menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, event.view) {
|
||||
views.add(it)
|
||||
}
|
||||
views.reversed().forEach { (event.view as ViewGroup).addView(it, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
if (childView.id == contextMenuButtonIconView) {
|
||||
menuMap[OperaViewerIcons::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
}
|
||||
|
||||
if (event.parent.id == componentsHolder && childView.id == feedNewChat) {
|
||||
menuMap[SettingsGearInjector::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (viewGroup !is LinearLayout && childView.id == chatActionMenu && context.isDeveloper) {
|
||||
event.view = LinearLayout(childView.context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(
|
||||
(menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).createDebugInfoView(childView.context)
|
||||
)
|
||||
addView(event.view)
|
||||
}
|
||||
}
|
||||
|
||||
if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && hasV2ActionMenu()) {
|
||||
(menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !hasV2ActionMenu()) {
|
||||
if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe
|
||||
menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (viewGroup !is LinearLayout && childView.id == actionMenu && messaging.lastFocusedConversationType == 1) {
|
||||
val injectedLayout = LinearLayout(childView.context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.BOTTOM
|
||||
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
addView(childView)
|
||||
}
|
||||
|
||||
event.parent.post {
|
||||
injectedLayout.addView(ScrollView(injectedLayout.context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
weight = 1f;
|
||||
setMargins(0, 100, 0, 0)
|
||||
}
|
||||
|
||||
addView(LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view ->
|
||||
view.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(0, 5, 0, 5)
|
||||
}
|
||||
addView(view)
|
||||
}
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
|
||||
event.view = injectedLayout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.res.use
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -74,12 +75,12 @@ class FriendFeedInfoMenu : AbstractMenu() {
|
||||
private val sigColorTextPrimary by lazy {
|
||||
context.androidContext.theme.obtainStyledAttributes(
|
||||
intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
|
||||
).getColor(0, 0)
|
||||
).use { it.getColor(0, 0) }
|
||||
}
|
||||
private val sigColorBackgroundSurface by lazy {
|
||||
context.androidContext.theme.obtainStyledAttributes(
|
||||
intArrayOf(context.resources.getIdentifier("sigColorBackgroundSurface", "attr"))
|
||||
).getColor(0, 0)
|
||||
).use { it.getColor(0, 0) }
|
||||
}
|
||||
|
||||
private fun getImageDrawable(url: String): Drawable {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.res.use
|
||||
import me.rhunk.snapenhance.common.ui.createComposeView
|
||||
import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride
|
||||
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
|
||||
@@ -170,7 +171,7 @@ class OperaContextActionMenu : AbstractMenu() {
|
||||
color = remember {
|
||||
view.context.theme.obtainStyledAttributes(
|
||||
intArrayOf(view.context.resources.getIdentifier("sigColorTextPrimary", "attr"))
|
||||
).getColor(0, 0).let { Color(it) }
|
||||
).use { Color(it.getColor(0, 0)) }
|
||||
},
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.res.use
|
||||
import me.rhunk.snapenhance.common.ui.OverlayType
|
||||
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
|
||||
import me.rhunk.snapenhance.core.util.ktx.getDimens
|
||||
@@ -65,8 +66,8 @@ class SettingsGearInjector : AbstractMenu() {
|
||||
gravity = android.view.Gravity.CENTER
|
||||
}
|
||||
setImageDrawable(context.resources.getDrawable("svg_settings_32x32", context.theme))
|
||||
context.resources.getStyledAttributes("headerButtonOpaqueIconTint", context.theme).getColorStateList(0)?.let {
|
||||
imageTintList = it
|
||||
context.resources.getStyledAttributes("headerButtonOpaqueIconTint", context.theme).use {
|
||||
imageTintList = it.getColorStateList(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user