feat: reply from notifications

This commit is contained in:
rhunk
2023-05-29 10:56:15 +02:00
parent 7ff124a9eb
commit 6f3251de51
13 changed files with 405 additions and 48 deletions

View File

@@ -11,6 +11,7 @@ import android.widget.Toast
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import me.rhunk.snapenhance.bridge.AbstractBridgeClient
import me.rhunk.snapenhance.data.MessageSender
import me.rhunk.snapenhance.database.DatabaseAccess
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.manager.impl.ActionManager
@@ -40,6 +41,7 @@ class ModContext {
val actionManager = ActionManager(this)
val database = DatabaseAccess(this)
val downloadServer = DownloadServer(this)
val messageSender = MessageSender(this)
val classCache get() = SnapEnhance.classCache
val resources: Resources get() = androidContext.resources

View File

@@ -169,7 +169,7 @@ enum class ConfigProperty(
"description.gallery_media_send_override",
ConfigCategory.EXTRAS,
ConfigStateSelection(
listOf("OFF", "NOTE", "SNAP"),
listOf("OFF", "NOTE", "SNAP", "LIVE_SNAP"),
"OFF"
)
),

View File

@@ -0,0 +1,164 @@
package me.rhunk.snapenhance.data
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.data.wrapper.impl.MessageDestinations
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.util.CallbackBuilder
import me.rhunk.snapenhance.util.protobuf.ProtoWriter
class MessageSender(
private val context: ModContext,
) {
companion object {
val redSnapProto: (Boolean) -> ByteArray = {hasAudio ->
ProtoWriter().apply {
write(11, 5) {
write(1) {
write(1) {
writeConstant(2, 0)
writeConstant(12, 0)
writeConstant(15, 0)
}
writeConstant(6, 0)
}
write(2) {
writeConstant(5, if (hasAudio) 1 else 0)
writeBuffer(6, byteArrayOf())
}
}
}.toByteArray()
}
val audioNoteProto: (Int) -> ByteArray = { duration ->
ProtoWriter().apply {
write(6, 1) {
write(1) {
writeConstant(2, 4)
write(5) {
writeConstant(1, 0)
writeConstant(2, 0)
}
writeConstant(7, 0)
writeConstant(13, duration)
}
}
}.toByteArray()
}
}
private val sendMessageCallback by lazy { context.mappings.getMappedClass("callbacks", "SendMessageCallback") }
private val platformAnalyticsCreatorClass by lazy {
context.mappings.getMappedClass("PlatformAnalyticsCreator")
}
private fun defaultPlatformAnalytics(): ByteArray {
val analyticsSource = platformAnalyticsCreatorClass.constructors[0].parameterTypes[0]
val chatAnalyticsSource = analyticsSource.enumConstants.first { it.toString() == "CHAT" }
val platformAnalyticsDefaultArgs = arrayOf(chatAnalyticsSource, null, null, null, null, null, null, null, null, null, 0L, 0L,
null, null, false, null, null, 0L, null, null, false, null, null,
null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, false, null, null, false, 0L, -2, 8191)
val platformAnalyticsInstance = platformAnalyticsCreatorClass.constructors[0].newInstance(
*platformAnalyticsDefaultArgs
) ?: throw Exception("Failed to create platform analytics instance")
return platformAnalyticsInstance.javaClass.declaredMethods.first { it.returnType == ByteArray::class.java }
.invoke(platformAnalyticsInstance) as ByteArray?
?: throw Exception("Failed to get platform analytics content")
}
private fun createLocalMessageContentTemplate(
contentType: ContentType,
messageContent: ByteArray,
localMediaReference: ByteArray? = null,
metricMessageMediaType: MetricsMessageMediaType = MetricsMessageMediaType.DERIVED_FROM_MESSAGE_TYPE,
metricsMediaType: MetricsMessageType = MetricsMessageType.TEXT,
savePolicy: String = "PROHIBITED",
): String {
return """
{
"mAllowsTranscription": false,
"mBotMention": false,
"mContent": [${messageContent.joinToString(",")}],
"mContentType": "${contentType.name}",
"mIncidentalAttachments": [],
"mLocalMediaReferences": [${
if (localMediaReference != null) {
"{\"mId\": [${localMediaReference.joinToString(",")}]}"
} else {
""
}
}],
"mPlatformAnalytics": {
"mAttemptId": {
"mId": [${(1..16).map { (-127 ..127).random() }.joinToString(",")}]
},
"mContent": [${defaultPlatformAnalytics().joinToString(",")}],
"mMetricsMessageMediaType": "${metricMessageMediaType.name}",
"mMetricsMessageType": "${metricsMediaType.name}",
"mReactionSource": "NONE"
},
"mSavePolicy": "$savePolicy"
}
""".trimIndent()
}
private fun internalSendMessage(conversations: List<SnapUUID>, localMessageContentTemplate: String, callback: Any) {
val sendMessageWithContentMethod = context.classCache.conversationManager.declaredMethods.first { it.name == "sendMessageWithContent" }
val localMessageContent = context.gson.fromJson(localMessageContentTemplate, context.classCache.localMessageContent)
val messageDestinations = MessageDestinations(AbstractWrapper.newEmptyInstance(context.classCache.messageDestinations)).also {
it.conversations = conversations
it.mPhoneNumbers = arrayListOf()
it.stories = arrayListOf()
}
sendMessageWithContentMethod.invoke(context.feature(Messaging::class).conversationManager, messageDestinations.instanceNonNull(), localMessageContent, callback)
}
//TODO: implement sendSnapMessage
/*
fun sendSnapMessage(conversations: List<SnapUUID>, chatMediaType: ChatMediaType, uri: Uri, onError: (Any) -> Unit = {}, onSuccess: () -> Unit = {}) {
val mediaReferenceBuffer = FlatBufferBuilder(0).apply {
val uriOffset = createString(uri.toString())
forceDefaults(true)
startTable(2)
addOffset(1, uriOffset, 0)
addInt(0, chatMediaType.value, 0)
finish(endTable())
finished()
}.sizedByteArray()
internalSendMessage(conversations, createLocalMessageContentTemplate(
contentType = ContentType.SNAP,
messageContent = redSnapProto(chatMediaType == ChatMediaType.AUDIO || chatMediaType == ChatMediaType.VIDEO),
localMediaReference = mediaReferenceBuffer,
metricMessageMediaType = MetricsMessageMediaType.IMAGE,
metricsMediaType = MetricsMessageType.SNAP
), CallbackBuilder(sendMessageCallback)
.override("onSuccess") {
onSuccess()
}
.override("onError") {
onError(it.arg(0))
}
.build())
}*/
fun sendChatMessage(conversations: List<SnapUUID>, message: String, onError: (Any) -> Unit = {}, onSuccess: () -> Unit = {}) {
internalSendMessage(conversations, createLocalMessageContentTemplate(ContentType.CHAT, ProtoWriter().apply {
write(2) {
writeString(1, message)
}
}.toByteArray(), savePolicy = "LIFETIME"), CallbackBuilder(sendMessageCallback)
.override("onSuccess", callback = { onSuccess() })
.override("onError", callback = { onError(it.arg(0)) })
.build())
}
}

View File

@@ -12,6 +12,8 @@ class SnapClassCache (
val messageUpdateEnum by lazy { findClass("com.snapchat.client.messaging.MessageUpdate") }
val unifiedGrpcService by lazy { findClass("com.snapchat.client.grpc.UnifiedGrpcService\$CppProxy") }
val networkApi by lazy { findClass("com.snapchat.client.network_api.NetworkApi\$CppProxy") }
val messageDestinations by lazy { findClass("com.snapchat.client.messaging.MessageDestinations") }
val localMessageContent by lazy { findClass("com.snapchat.client.messaging.LocalMessageContent") }
private fun findClass(className: String): Class<*> {
return try {

View File

@@ -4,6 +4,39 @@ enum class MessageState {
PREPARING, SENDING, COMMITTED, FAILED, CANCELING
}
enum class ChatMediaType (
val value: Int
) {
IMAGE(0),
VIDEO(1),
VIDEO_NO_SOUND(2),
FRIEND_DEPRECATED(3),
BLOB(4),
LAGUNA_SOUND(5),
LAGUNA_NO_SOUND(6),
GIF(7),
FINGERPRINT_HEADER_SIZE(8),
AUDIO_STITCH(9),
PSYCHOMANTIS(10),
SCREAMINGMANTIS(11),
MALIBU_SOUND(12),
MALIBU_NO_SOUND(13),
LAGUNAHD_SOUND(14),
LAGUNAHD_NO_SOUND(15),
GHOSTMANTIS(16),
NEWPORT_SOUND(17),
NEWPORT_NO_SOUND(18),
AUDIO(19),
BLOOP(20),
SPECTACLES_IMAGE(21),
SPECTACLES_VIDEO(22),
SPECTACLES_VIDEO_NO_SOUND(23),
CHEERIOS_IMAGE(24),
CHEERIOS_VIDEO_SOUND(25),
CHEERIOS_VIDEO_NO_SOUND(26),
WEB(27),
UNRECOGNIZED_VALUE(-9999);
}
enum class ContentType(val id: Int) {
UNKNOWN(-1),
SNAP(0),
@@ -36,6 +69,66 @@ enum class PlayableSnapState {
NOTDOWNLOADED, DOWNLOADING, DOWNLOADFAILED, PLAYABLE, VIEWEDREPLAYABLE, PLAYING, VIEWEDNOTREPLAYABLE
}
enum class MetricsMessageMediaType {
NO_MEDIA,
IMAGE,
VIDEO,
VIDEO_NO_SOUND,
GIF,
DERIVED_FROM_MESSAGE_TYPE,
REACTION
}
enum class MetricsMessageType {
TEXT,
STICKER,
CUSTOM_STICKER,
SNAP,
AUDIO_NOTE,
MEDIA,
BATCHED_MEDIA,
MISSED_AUDIO_CALL,
MISSED_VIDEO_CALL,
JOINED_CALL,
LEFT_CALL,
SNAPCHATTER,
LOCATION_SHARE,
LOCATION_REQUEST,
SCREENSHOT,
SCREEN_RECORDING,
GAME_CLOSED,
STORY_SHARE,
MAP_DROP_SHARE,
MAP_STORY_SHARE,
MAP_STORY_SNAP_SHARE,
MAP_HEAT_SNAP_SHARE,
MAP_SCREENSHOT_SHARE,
MEMORIES_STORY,
SEARCH_STORY_SHARE,
SEARCH_STORY_SNAP_SHARE,
DISCOVER_SHARE,
SHAZAM_SHARE,
SAVE_TO_CAMERA_ROLL,
GAME_SCORE_SHARE,
SNAP_PRO_PROFILE_SHARE,
SNAP_PRO_SNAP_SHARE,
CANVAS_APP_SHARE,
AD_SHARE,
STORY_REPLY,
SPOTLIGHT_STORY_SHARE,
CAMEO,
MEMOJI,
BITMOJI_OUTFIT_SHARE,
LIVE_LOCATION_SHARE,
CREATIVE_TOOL_ITEM,
SNAP_KIT_INVITE_SHARE,
QUOTE_REPLY_SHARE,
BLOOPS_STORY_SHARE,
SNAP_PRO_SAVED_STORY_SHARE,
PLACE_PROFILE_SHARE,
PLACE_STORY_SHARE,
SAVED_STORY_SHARE
}
enum class MediaReferenceType {
UNASSIGNED, OVERLAY, IMAGE, VIDEO, ASSET_BUNDLE, AUDIO, ANIMATED_IMAGE, FONT, WEB_VIEW_CONTENT, VIDEO_NO_AUDIO
}

View File

@@ -1,10 +1,17 @@
package me.rhunk.snapenhance.data.wrapper
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.util.CallbackBuilder
abstract class AbstractWrapper(
protected var instance: Any?
) {
companion object {
fun newEmptyInstance(clazz: Class<*>): Any {
return CallbackBuilder.createEmptyObject(clazz.constructors[0]) ?: throw NullPointerException()
}
}
fun instanceNonNull(): Any = instance!!
fun isPresent(): Boolean = instance == null

View File

@@ -0,0 +1,14 @@
package me.rhunk.snapenhance.data.wrapper.impl
import me.rhunk.snapenhance.data.wrapper.AbstractWrapper
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.setObjectField
class MessageDestinations(obj: Any) : AbstractWrapper(obj){
var conversations get() = (instanceNonNull().getObjectField("mConversations") as ArrayList<*>).map { SnapUUID(it) }
set(value) = instanceNonNull().setObjectField("mConversations", value.map { it.instanceNonNull() }.toCollection(ArrayList()))
var stories get() = instanceNonNull().getObjectField("mStories") as ArrayList<Any>
set(value) = instanceNonNull().setObjectField("mStories", value)
var mPhoneNumbers get() = instanceNonNull().getObjectField("mPhoneNumbers") as ArrayList<Any>
set(value) = instanceNonNull().setObjectField("mPhoneNumbers", value)
}

View File

@@ -1,51 +1,16 @@
package me.rhunk.snapenhance.features.impl.extras
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.MessageSender
import me.rhunk.snapenhance.data.wrapper.impl.MessageContent
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.util.protobuf.ProtoReader
import me.rhunk.snapenhance.util.protobuf.ProtoWriter
class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadParams = FeatureLoadParams.INIT_SYNC) {
private val redSnapProto: ByteArray by lazy {
ProtoWriter().apply {
write(11, 5) {
write(1) {
write(1) {
writeConstant(2, 0)
writeConstant(12, 0)
writeConstant(15, 0)
}
writeConstant(6, 0)
}
write(2) {
writeConstant(5, 0)
writeBuffer(6, byteArrayOf())
}
}
}.toByteArray()
}
private val audioNoteProto: (Int) -> ByteArray = { duration ->
ProtoWriter().apply {
write(6, 1) {
write(1) {
writeConstant(2, 4)
write(5) {
writeConstant(1, 0)
writeConstant(2, 0)
}
writeConstant(7, 0)
writeConstant(13, duration)
}
}
}.toByteArray()
}
override fun init() {
Hooker.hook(context.classCache.conversationManager, "sendMessageWithContent", HookStage.BEFORE) { param ->
@@ -56,15 +21,15 @@ class GalleryMediaSendOverride : Feature("Gallery Media Send Override", loadPara
val messageProtoReader = ProtoReader(localMessageContent.content)
if (messageProtoReader.exists(7)) return@hook
when (context.config.state(ConfigProperty.GALLERY_MEDIA_SEND_OVERRIDE)) {
"SNAP" -> {
when (val overrideType = context.config.state(ConfigProperty.GALLERY_MEDIA_SEND_OVERRIDE)) {
"SNAP", "LIVE_SNAP" -> {
localMessageContent.contentType = ContentType.SNAP
localMessageContent.content = redSnapProto
localMessageContent.content = MessageSender.redSnapProto(overrideType == "LIVE_SNAP")
}
"NOTE" -> {
localMessageContent.contentType = ContentType.NOTE
val mediaDuration = messageProtoReader.getInt(3, 3, 5, 1, 1, 15) ?: 0
localMessageContent.content = audioNoteProto(mediaDuration)
localMessageContent.content = MessageSender.audioNoteProto(mediaDuration)
}
}
}

View File

@@ -2,7 +2,10 @@ package me.rhunk.snapenhance.features.impl.extras
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.RemoteInput
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.os.UserHandle
@@ -28,10 +31,18 @@ import me.rhunk.snapenhance.util.PreviewUtils
import me.rhunk.snapenhance.util.protobuf.ProtoReader
class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) {
companion object{
const val ACTION_REPLY = "me.rhunk.snapenhance.action.REPLY"
}
private val notificationDataQueue = mutableMapOf<Long, NotificationData>() // messageId => notification
private val cachedMessages = mutableMapOf<String, MutableList<String>>() // conversationId => cached messages
private val notificationIdMap = mutableMapOf<Int, String>() // notificationId => conversationId
private val broadcastReceiverClass by lazy {
context.androidContext.classLoader.loadClass("com.snap.widgets.core.BestFriendsWidgetProvider")
}
private val notifyAsUserMethod by lazy {
XposedHelpers.findMethodExact(
NotificationManager::class.java, "notifyAsUser",
@@ -54,8 +65,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
context.androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
private fun setNotificationText(notification: NotificationData, text: String) {
with(notification.notification.extras) {
private fun setNotificationText(notification: Notification, text: String) {
with(notification.extras) {
putString("android.text", text)
putString("android.bigText", text)
}
@@ -70,6 +81,74 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
return messageBuilder.toString()
}
private fun setupNotificationActionButtons(conversationId: String, notificationData: NotificationData) {
val notificationBuilder = XposedHelpers.newInstance(
Notification.Builder::class.java,
context.androidContext,
notificationData.notification
) as Notification.Builder
val chatReplyInput = RemoteInput.Builder("chat_reply_input")
.setLabel("Reply")
.build()
val replyIntent = Intent()
.setClassName(Constants.SNAPCHAT_PACKAGE_NAME, broadcastReceiverClass.name)
.putExtra("conversation_id", conversationId)
.putExtra("notification_id", notificationData.id)
.setAction(ACTION_REPLY)
val action = Notification.Action.Builder(
null,
"Reply",
PendingIntent.getBroadcast(
context.androidContext,
0,
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
).addRemoteInput(chatReplyInput).build()
notificationBuilder.setActions(action)
notificationData.notification = notificationBuilder.build()
}
private fun setupBroadcastReceiverHook() {
Hooker.hook(broadcastReceiverClass, "onReceive", HookStage.BEFORE) { param ->
val androidContext = param.arg<Context>(0)
val intent = param.arg<Intent>(1)
if (intent.action != ACTION_REPLY) return@hook
param.setResult(null)
val notificationManager = androidContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val updateNotification: (Int, (Notification) -> Unit) -> Unit = { notificationId, notificationBuilder ->
notificationManager.activeNotifications.firstOrNull { it.id == notificationId }?.let {
notificationBuilder(it.notification)
XposedBridge.invokeOriginalMethod(notifyAsUserMethod, notificationManager, arrayOf(
it.tag, it.id, it.notification, it.user
))
}
}
val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input")
.toString()
val conversationId = intent.getStringExtra("conversation_id")!!
val notificationId = intent.getIntExtra("notification_id", -1)
context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser ->
cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input")
updateNotification(notificationId) { notification ->
setNotificationText(notification, computeNotificationText(conversationId))
}
context.messageSender.sendChatMessage(listOf(SnapUUID.fromString(conversationId)), input, onError = {
context.longToast("Failed to send message: $it")
})
}
}
}
private fun fetchMessagesResult(conversationId: String, messages: List<Message>) {
val sendNotificationData = { notificationData: NotificationData, forceCreate: Boolean ->
val notificationId = if (forceCreate) System.nanoTime().toInt() else notificationData.id
@@ -89,7 +168,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
val formatUsername: (String) -> String = { "$senderUsername: $it" }
val notificationCache = cachedMessages.let { it.computeIfAbsent(conversationId) { mutableListOf() } }
val appendNotifications: () -> Unit = { setNotificationText(notificationData, computeNotificationText(conversationId))}
val appendNotifications: () -> Unit = { setNotificationText(notificationData.notification, computeNotificationText(conversationId))}
when (contentType) {
ContentType.NOTE -> {
@@ -150,6 +229,10 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
}
}
if (contentType == ContentType.CHAT) {
setupNotificationActionButtons(conversationId, notificationData)
}
sendNotificationData(notificationData, false)
}.clear()
}
@@ -165,6 +248,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
}
override fun init() {
setupBroadcastReceiverHook()
val fetchConversationWithMessagesCallback = context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback")
Hooker.hook(notifyAsUserMethod, HookStage.BEFORE) { param ->

View File

@@ -1,15 +1,12 @@
package me.rhunk.snapenhance.features.impl.privacy
import de.robv.android.xposed.XposedHelpers
import me.rhunk.snapenhance.Logger.debug
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.hook.HookAdapter
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import java.nio.charset.StandardCharsets
import java.util.Base64
class DisableMetrics : Feature("DisableMetrics", loadParams = FeatureLoadParams.INIT_SYNC) {
override fun init() {

View File

@@ -17,6 +17,7 @@ import me.rhunk.snapenhance.mapping.impl.CallbackMapper
import me.rhunk.snapenhance.mapping.impl.EnumMapper
import me.rhunk.snapenhance.mapping.impl.GridMediaItemMapper
import me.rhunk.snapenhance.mapping.impl.OperaPageViewControllerMapper
import me.rhunk.snapenhance.mapping.impl.PlatformAnalyticsCreatorMapper
import me.rhunk.snapenhance.mapping.impl.PlusSubscriptionMapper
import me.rhunk.snapenhance.util.getObjectField
import java.nio.charset.StandardCharsets
@@ -31,6 +32,7 @@ class MappingManager(private val context: ModContext) : Manager {
add(PlusSubscriptionMapper())
add(GridMediaItemMapper())
add(BCryptClassMapper())
add(PlatformAnalyticsCreatorMapper())
}
private val mappings = ConcurrentHashMap<String, Any>()

View File

@@ -0,0 +1,26 @@
package me.rhunk.snapenhance.mapping.impl
import me.rhunk.snapenhance.mapping.Mapper
class PlatformAnalyticsCreatorMapper : Mapper() {
override fun useClasses(
classLoader: ClassLoader,
classes: List<Class<*>>,
mappings: MutableMap<String, Any>
) {
for (clazz in classes) {
if (clazz.isEnum || clazz.isInterface) continue
val constructors = clazz.constructors
if (constructors.isEmpty()) continue
val firstConstructor = constructors[0]
// 47 is the number of parameters of the constructor
// can change in future versions
if (firstConstructor.parameterCount != 47) continue
if (!firstConstructor.parameterTypes[0].isEnum) continue
if (firstConstructor.parameterTypes[0].enumConstants.none { it.toString() == "IN_APP_NOTIFICATION" }) continue
mappings["PlatformAnalyticsCreator"] = clazz.name
return
}
}
}

View File

@@ -69,7 +69,7 @@ class CallbackBuilder(
}
companion object {
private fun createEmptyObject(constructor: Constructor<*>): Any? {
fun createEmptyObject(constructor: Constructor<*>): Any? {
//compute the args for the constructor with null or default primitive values
val args = constructor.parameterTypes.map { type: Class<*> ->
if (type.isPrimitive) {