feat: more profile information

- refactor DatabaseAccess
- refactor wasInjectedView to ViewTagState
- refactor applyTheme ktx
This commit is contained in:
rhunk 2023-09-24 00:22:03 +02:00
parent 2c16f41fd6
commit a341801be3
11 changed files with 185 additions and 97 deletions

View File

@ -672,10 +672,28 @@
"profile_info": {
"title": "Profile Info",
"username": "Username",
"first_created_username": "First Created Username",
"mutable_username": "Mutable Username",
"display_name": "Display Name",
"added_date": "Added Date",
"birthday": "Birthday : {month} {day}"
"birthday": "Birthday : {month} {day}",
"friendship": "Friendship",
"add_source": "Add Source",
"snapchat_plus": "Snapchat Plus",
"snapchat_plus_state": {
"subscribed": "Subscribed",
"not_subscribed": "Not Subscribed"
},
"friendship_link_type": {
"mutual": "Mutual",
"outgoing": "Outgoing",
"blocked": "Blocked",
"deleted": "Deleted",
"following": "Following",
"suggested": "Suggested",
"incoming": "Incoming",
"incoming_follower": "Incoming Follower"
}
},
"chat_export": {

View File

@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.database.objects.FriendFeedEntry
import me.rhunk.snapenhance.core.database.objects.FriendInfo
import me.rhunk.snapenhance.core.database.objects.StoryEntry
import me.rhunk.snapenhance.core.database.objects.UserConversationLink
import me.rhunk.snapenhance.core.util.ktx.getStringOrNull
import me.rhunk.snapenhance.manager.Manager
import java.io.File
@ -66,19 +67,16 @@ class DatabaseAccess(private val context: ModContext) : Manager {
table: String,
where: String,
args: Array<String>
): T? {
val cursor = database.rawQuery("SELECT * FROM $table WHERE $where", args)
if (!cursor.moveToFirst()) {
cursor.close()
): T? = database.rawQuery("SELECT * FROM $table WHERE $where", args).use {
if (!it.moveToFirst()) {
return null
}
try {
obj.write(cursor)
obj.write(it)
} catch (e: Throwable) {
context.log.error("Failed to read database object", e)
}
cursor.close()
return obj
obj
}
fun getFeedEntryByUserId(userId: String): FriendFeedEntry? {
@ -95,18 +93,14 @@ class DatabaseAccess(private val context: ModContext) : Manager {
val myUserId by lazy {
safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase ->
val cursor = arroyoDatabase.rawQuery(buildString {
arroyoDatabase.rawQuery(buildString {
append("SELECT * FROM required_values WHERE key = 'USERID'")
}, null)
if (!cursor.moveToFirst()) {
cursor.close()
return@safeDatabaseOperation null
}, null).use { query ->
if (!query.moveToFirst()) {
return@safeDatabaseOperation null
}
query.getStringOrNull("value")!!
}
val userId = cursor.getString(cursor.getColumnIndex("value"))
cursor.close()
userId
}!!
}
@ -136,20 +130,20 @@ class DatabaseAccess(private val context: ModContext) : Manager {
fun getFeedEntries(limit: Int): List<FriendFeedEntry> {
return safeDatabaseOperation(openMain()) { database ->
val cursor = database.rawQuery(
database.rawQuery(
"SELECT * FROM FriendsFeedView ORDER BY _id LIMIT ?",
arrayOf(limit.toString())
)
val list = mutableListOf<FriendFeedEntry>()
while (cursor.moveToNext()) {
val friendFeedEntry = FriendFeedEntry()
try {
friendFeedEntry.write(cursor)
} catch (_: Throwable) {}
list.add(friendFeedEntry)
).use { query ->
val list = mutableListOf<FriendFeedEntry>()
while (query.moveToNext()) {
val friendFeedEntry = FriendFeedEntry()
try {
friendFeedEntry.write(query)
} catch (_: Throwable) {}
list.add(friendFeedEntry)
}
list
}
cursor.close()
list
} ?: emptyList()
}
@ -166,18 +160,16 @@ class DatabaseAccess(private val context: ModContext) : Manager {
}
fun getConversationType(conversationId: String): Int? {
return safeDatabaseOperation(openArroyo()) {
val cursor = it.rawQuery(
return safeDatabaseOperation(openArroyo()) { database ->
database.rawQuery(
"SELECT * FROM user_conversation WHERE client_conversation_id = ?",
arrayOf(conversationId)
)
if (!cursor.moveToFirst()) {
cursor.close()
return@safeDatabaseOperation null
).use { query ->
if (!query.moveToFirst()) {
return@safeDatabaseOperation null
}
query.getInt(query.getColumnIndex("conversation_type"))
}
val type = cursor.getInt(cursor.getColumnIndex("conversation_type"))
cursor.close()
type
}
}
@ -195,16 +187,19 @@ class DatabaseAccess(private val context: ModContext) : Manager {
fun getDMOtherParticipant(conversationId: String): String? {
return safeDatabaseOperation(openArroyo()) { cursor ->
val query = cursor.rawQuery(
cursor.rawQuery(
"SELECT * FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0",
arrayOf(conversationId)
)
val participants = mutableListOf<String>()
while (query.moveToNext()) {
participants.add(query.getString(query.getColumnIndex("user_id")))
).use { query ->
val participants = mutableListOf<String>()
if (!query.moveToFirst()) {
return@safeDatabaseOperation null
}
do {
participants.add(query.getString(query.getColumnIndex("user_id")))
} while (query.moveToNext())
participants.firstOrNull { it != myUserId }
}
query.close()
participants.firstOrNull { it != myUserId }
}
}
@ -217,20 +212,19 @@ class DatabaseAccess(private val context: ModContext) : Manager {
fun getConversationParticipants(conversationId: String): List<String>? {
return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase ->
val cursor = arroyoDatabase.rawQuery(
arroyoDatabase.rawQuery(
"SELECT * FROM user_conversation WHERE client_conversation_id = ?",
arrayOf(conversationId)
)
if (!cursor.moveToFirst()) {
cursor.close()
return@safeDatabaseOperation emptyList()
).use {
if (!it.moveToFirst()) {
return@safeDatabaseOperation null
}
val participants = mutableListOf<String>()
do {
participants.add(it.getString(it.getColumnIndex("user_id")))
} while (it.moveToNext())
participants
}
val participants = mutableListOf<String>()
do {
participants.add(cursor.getString(cursor.getColumnIndex("user_id")))
} while (cursor.moveToNext())
cursor.close()
participants
}
}
@ -239,22 +233,35 @@ class DatabaseAccess(private val context: ModContext) : Manager {
limit: Int
): List<ConversationMessage>? {
return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase ->
val cursor = arroyoDatabase.rawQuery(
arroyoDatabase.rawQuery(
"SELECT * FROM conversation_message WHERE client_conversation_id = ? ORDER BY creation_timestamp DESC LIMIT ?",
arrayOf(conversationId, limit.toString())
)
if (!cursor.moveToFirst()) {
cursor.close()
return@safeDatabaseOperation emptyList()
).use { query ->
if (!query.moveToFirst()) {
return@safeDatabaseOperation null
}
val messages = mutableListOf<ConversationMessage>()
do {
val message = ConversationMessage()
message.write(query)
messages.add(message)
} while (query.moveToNext())
messages
}
}
}
fun getAddSource(userId: String): String? {
return safeDatabaseOperation(openMain()) { database ->
database.rawQuery(
"SELECT addSource FROM FriendWhoAddedMe WHERE userId = ?",
arrayOf(userId)
).use {
if (!it.moveToFirst()) {
return@safeDatabaseOperation null
}
it.getStringOrNull("addSource")
}
val messages = mutableListOf<ConversationMessage>()
do {
val message = ConversationMessage()
message.write(cursor)
messages.add(message)
} while (cursor.moveToNext())
cursor.close()
messages
}
}
}

View File

@ -30,8 +30,13 @@ data class FriendInfo(
var reverseBestFriendRanking: Int = 0,
var isPinnedBestFriend: Int = 0,
var plusBadgeVisibility: Int = 0,
var usernameForSorting: String? = null
var usernameForSorting: String? = null,
var friendLinkType: Int = 0,
var postViewEmoji: String? = null,
) : DatabaseObject, SerializableDataObject() {
val mutableUsername get() = username?.split("|")?.last()
val firstCreatedUsername get() = username?.split("|")?.first()
@SuppressLint("Range")
override fun write(cursor: Cursor) {
with(cursor) {
@ -55,10 +60,13 @@ data class FriendInfo(
streakExpirationTimestamp = getLong("streakExpiration")
reverseBestFriendRanking = getInteger("reverseBestFriendRanking")
usernameForSorting = getStringOrNull("usernameForSorting")
friendLinkType = getInteger("friendLinkType")
postViewEmoji = getStringOrNull("postViewEmoji")
if (getColumnIndex("isPinnedBestFriend") != -1) isPinnedBestFriend =
getInteger("isPinnedBestFriend")
if (getColumnIndex("plusBadgeVisibility") != -1) plusBadgeVisibility =
getInteger("plusBadgeVisibility")
}
}
}

View File

@ -133,3 +133,20 @@ enum class MetricsMessageType {
enum class MediaReferenceType {
UNASSIGNED, OVERLAY, IMAGE, VIDEO, ASSET_BUNDLE, AUDIO, ANIMATED_IMAGE, FONT, WEB_VIEW_CONTENT, VIDEO_NO_AUDIO
}
enum class FriendLinkType(val value: Int, val shortName: String) {
MUTUAL(0, "mutual"),
OUTGOING(1, "outgoing"),
BLOCKED(2, "blocked"),
DELETED(3, "deleted"),
FOLLOWING(4, "following"),
SUGGESTED(5, "suggested"),
INCOMING(6, "incoming"),
INCOMING_FOLLOWER(7, "incoming_follower");
companion object {
fun fromValue(value: Int): FriendLinkType {
return values().firstOrNull { it.value == value } ?: MUTUAL
}
}
}

View File

@ -15,6 +15,10 @@ import android.widget.Switch
import android.widget.TextView
import me.rhunk.snapenhance.Constants
fun View.applyTheme(componentWidth: Int? = null, hasRadius: Boolean = false, isAmoled: Boolean = true) {
ViewAppearanceHelper.applyTheme(this, componentWidth, hasRadius, isAmoled)
}
object ViewAppearanceHelper {
@SuppressLint("UseSwitchCompatOrMaterialCode", "RtlHardcoded", "DiscouragedApi",
"ClickableViewAccessibility"

View File

@ -0,0 +1,20 @@
package me.rhunk.snapenhance.ui
import android.view.View
import kotlin.random.Random
class ViewTagState {
private val tag = Random.nextInt(0x7000000, 0x7FFFFFFF)
operator fun get(view: View) = hasState(view)
private fun hasState(view: View): Boolean {
if (view.getTag(tag) != null) return true
view.setTag(tag, true)
return false
}
fun removeState(view: View) {
view.setTag(tag, null)
}
}

View File

@ -2,7 +2,7 @@ package me.rhunk.snapenhance.ui.menu
import me.rhunk.snapenhance.ModContext
abstract class AbstractMenu() {
abstract class AbstractMenu {
lateinit var context: ModContext
open fun init() {}

View File

@ -12,23 +12,18 @@ import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.ViewTagState
import me.rhunk.snapenhance.ui.applyTheme
import me.rhunk.snapenhance.ui.menu.AbstractMenu
class ChatActionMenu : AbstractMenu() {
private val viewInjectedTag = 0x7FFFFF02
private fun wasInjectedView(view: View): Boolean {
if (view.getTag(viewInjectedTag) != null) return true
view.setTag(viewInjectedTag, true)
return false
}
private val viewTagState = ViewTagState()
@SuppressLint("SetTextI18n", "DiscouragedApi")
fun inject(viewGroup: ViewGroup) {
val parent = viewGroup.parent.parent as ViewGroup
if (wasInjectedView(parent)) return
if (viewTagState[parent]) return
//close the action menu using a touch event
val closeActionMenu = {
viewGroup.dispatchTouchEvent(
@ -73,7 +68,7 @@ class ChatActionMenu : AbstractMenu() {
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
ViewAppearanceHelper.applyTheme(this@layout, parent.width, true)
applyTheme(parent.width, true)
setMargins(chatActionMenuItemMargin, 0, chatActionMenuItemMargin, defaultGap)
}
}
@ -91,9 +86,8 @@ class ChatActionMenu : AbstractMenu() {
})
}
ViewAppearanceHelper.applyTheme(button, parent.width, true)
with(button) {
applyTheme(parent.width, true)
layoutParams = MarginLayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT

View File

@ -16,12 +16,14 @@ import me.rhunk.snapenhance.core.database.objects.FriendInfo
import me.rhunk.snapenhance.core.database.objects.UserConversationLink
import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.FriendLinkType
import me.rhunk.snapenhance.features.MessagingRuleFeature
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.spying.StealthMode
import me.rhunk.snapenhance.features.impl.tweaks.AutoSave
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.applyTheme
import me.rhunk.snapenhance.ui.menu.AbstractMenu
import java.net.HttpURLConnection
import java.net.URL
@ -59,6 +61,8 @@ class FriendFeedInfoMenu : AbstractMenu() {
context.log.error("Error loading bitmoji selfie", e)
}
val finalIcon = icon
val translation = context.translation.getCategory("profile_info")
context.runOnUiThread {
val addedTimestamp: Long = profile.addedTimestamp.coerceAtLeast(profile.reverseAddedTimestamp)
val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
@ -67,11 +71,13 @@ class FriendFeedInfoMenu : AbstractMenu() {
val birthday = Calendar.getInstance()
birthday[Calendar.MONTH] = (profile.birthday shr 32).toInt() - 1
val message: String = """
${context.translation["profile_info.username"]}: ${profile.username}
${context.translation["profile_info.display_name"]}: ${profile.displayName}
${context.translation["profile_info.added_date"]}: ${formatDate(addedTimestamp)}
${birthday.getDisplayName(
builder.setMessage(mapOf(
translation["first_created_username"] to profile.firstCreatedUsername,
translation["mutable_username"] to profile.mutableUsername,
translation["display_name"] to profile.displayName,
translation["added_date"] to formatDate(addedTimestamp),
null to birthday.getDisplayName(
Calendar.MONTH,
Calendar.LONG,
context.translation.loadedLocale
@ -79,9 +85,18 @@ class FriendFeedInfoMenu : AbstractMenu() {
context.translation.format("profile_info.birthday",
"month" to it,
"day" to profile.birthday.toInt().toString())
}}
""".trimIndent()
builder.setMessage(message)
},
translation["friendship"] to run {
translation.getCategory("friendship_link_type")[FriendLinkType.fromValue(profile.friendLinkType).shortName]
},
translation["add_source"] to context.database.getAddSource(profile.userId!!)?.takeIf { it.isNotEmpty() },
translation["snapchat_plus"] to run {
translation.getCategory("snapchat_plus_state")[if (profile.postViewEmoji != null) "subscribed" else "not_subscribed"]
}
).filterValues { it != null }.map {
line -> "${line.key?.let { "$it: " } ?: ""}${line.value}"
}.joinToString("\n"))
builder.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
@ -198,7 +213,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
val switch = Switch(context.androidContext)
switch.text = context.translation[text]
switch.isChecked = isChecked()
ViewAppearanceHelper.applyTheme(switch)
switch.applyTheme()
switch.setOnCheckedChangeListener { _: CompoundButton?, checked: Boolean ->
toggle(checked)
}
@ -218,7 +233,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
val previewButton = Button(viewModel.context).apply {
text = modContext.translation["friend_menu_option.preview"]
ViewAppearanceHelper.applyTheme(this, viewModel.width)
applyTheme(viewModel.width)
setOnClickListener {
showPreview(
targetUser,

View File

@ -11,10 +11,13 @@ import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.ui.ViewTagState
import java.lang.reflect.Modifier
@SuppressLint("DiscouragedApi")
class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
private val viewTagState = ViewTagState()
private val friendFeedInfoMenu = FriendFeedInfoMenu()
private val operaContextActionMenu = OperaContextActionMenu()
private val chatActionMenu = ChatActionMenu()
@ -114,15 +117,17 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
viewGroup.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
//context.config.writeConfig()
viewTagState.removeState(viewGroup)
}
})
viewTagState[viewGroup]
return@subscribe
}
if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@subscribe
//filter by the slot index
if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@subscribe
if (viewTagState[viewGroup]) return@subscribe
friendFeedInfoMenu.inject(viewGroup, originalAddView)
}
}

View File

@ -9,7 +9,7 @@ import android.widget.LinearLayout
import android.widget.ScrollView
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.ui.ViewAppearanceHelper.applyTheme
import me.rhunk.snapenhance.ui.applyTheme
import me.rhunk.snapenhance.ui.menu.AbstractMenu
@SuppressLint("DiscouragedApi")
@ -71,7 +71,7 @@ class OperaContextActionMenu : AbstractMenu() {
val button = Button(childView.getContext())
button.text = context.translation["opera_context_menu.download"]
button.setOnClickListener { context.feature(MediaDownloader::class).downloadLastOperaMediaAsync() }
applyTheme(button, isAmoled = false)
button.applyTheme(isAmoled = false)
linearLayout.addView(button)
(childView as ViewGroup).addView(linearLayout, 0)
} catch (e: Throwable) {