diff --git a/android-core/src/main/java/com/mparticle/SdkListener.java b/android-core/src/main/java/com/mparticle/SdkListener.java index f11103781..2736307c6 100644 --- a/android-core/src/main/java/com/mparticle/SdkListener.java +++ b/android-core/src/main/java/com/mparticle/SdkListener.java @@ -15,7 +15,7 @@ /** * Note: This is an Experimental feature. Adding an instance of SdkListener will slow down the SDK and * should be used only in development. - * + *

* SdkListener is a new feature which enables updates on and visibility into internal Events occuring * inside the SDK. */ @@ -123,7 +123,7 @@ public void onKitStarted(int kitId) { * @param kitId the id of the kit, corresponse with a {@link com.mparticle.MParticle.ServiceProviders} * @param reason a message containing the reason a kit was stopped */ - public void onKitExcluded(int kitId, @NonNull String reason) { + public void onKitExcluded(int kitId, @Nullable String reason) { } diff --git a/android-core/src/main/java/com/mparticle/internal/listeners/ApiClass.java b/android-core/src/main/java/com/mparticle/internal/listeners/ApiClass.java deleted file mode 100644 index 0812efcf3..000000000 --- a/android-core/src/main/java/com/mparticle/internal/listeners/ApiClass.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.mparticle.internal.listeners; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.CLASS) -@Target(ElementType.TYPE) -public @interface ApiClass { -} diff --git a/android-core/src/main/java/com/mparticle/internal/listeners/GraphListener.java b/android-core/src/main/java/com/mparticle/internal/listeners/GraphListener.java deleted file mode 100644 index 973a9fe94..000000000 --- a/android-core/src/main/java/com/mparticle/internal/listeners/GraphListener.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.mparticle.internal.listeners; - -import android.os.Message; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -interface GraphListener { - void onCompositeObjects(@Nullable Object child, @Nullable Object parent); - - void onThreadMessage(@NonNull String handlerName, @Nullable Message msg, boolean onNewThread, @Nullable StackTraceElement[] stackTrace); -} diff --git a/android-core/src/main/java/com/mparticle/internal/listeners/InternalListenerManager.java b/android-core/src/main/java/com/mparticle/internal/listeners/InternalListenerManager.java deleted file mode 100644 index 4b8d456db..000000000 --- a/android-core/src/main/java/com/mparticle/internal/listeners/InternalListenerManager.java +++ /dev/null @@ -1,376 +0,0 @@ -package com.mparticle.internal.listeners; - -import android.content.ContentValues; -import android.content.Context; -import android.os.Message; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.mparticle.SdkListener; -import com.mparticle.identity.AliasResponse; -import com.mparticle.internal.InternalSession; -import com.mparticle.internal.KitFrameworkWrapper; -import com.mparticle.internal.MPUtility; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.lang.ref.WeakReference; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class InternalListenerManager implements InternalListener { - private static InternalListenerManager instance = null; - private static final String INTERNAL_LISTENER_PROP = "debug.mparticle.listener"; - private Context context; - final List> sdkListeners = new ArrayList>(); - final List> graphListeners = new ArrayList>(); - private boolean thrown = false; - - private InternalListenerManager(Context context) { - this.context = context; - } - - @Nullable - public static InternalListenerManager start(Context context) { - boolean canRun = MPUtility.isAppDebuggable(context) || context.getPackageName().equals(MPUtility.getProp(INTERNAL_LISTENER_PROP)); - if (instance == null && context != null && canRun) { - instance = new InternalListenerManager(context.getApplicationContext()); - } - return instance; - } - - public static boolean isEnabled() { - return instance != null && - instance.hasListeners(); - } - - @NonNull - public static InternalListener getListener() { - if (instance != null && isEnabled()) { - return instance; - } else { - return InternalListener.EMPTY; - } - } - - public void addListener(SdkListener sdkListener) { - for (WeakReference listener : sdkListeners) { - if (listener.get() == sdkListener) { - return; - } - } - sdkListeners.add(new WeakReference(sdkListener)); - if (sdkListener instanceof GraphListener) { - graphListeners.add(new WeakReference((GraphListener) sdkListener)); - } - } - - public void removeListener(SdkListener sdkListener) { - for (WeakReference listener : new ArrayList>(sdkListeners)) { - if (listener.get() == sdkListener) { - sdkListeners.remove(listener); - } - } - for (WeakReference listener : new ArrayList>(graphListeners)) { - if (listener.get() == sdkListener) { - graphListeners.remove(listener); - } - } - } - - @Override - public void onApiCalled(final Object... objects) { - - } - - @Override - public void onKitApiCalled(int kitId, Boolean used, Object... objects) { - StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); - String methodName = null; - for (int i = 0; i < stackTrace.length; i++) { - StackTraceElement element = stackTrace[i]; - if (element.getClassName().equals(KitFrameworkWrapper.class.getName())) { - methodName = element.getMethodName() + "()"; - } - } - onKitApiCalled(stackTrace, methodName, kitId, used, objects); - } - - @Override - public void onKitApiCalled(String methodName, int kitId, Boolean used, Object... objects) { - onKitApiCalled(Thread.currentThread().getStackTrace(), methodName, kitId, used, objects); - } - - private void onKitApiCalled(StackTraceElement[] stackTrace, final String methodName, final int kitId, final boolean used, Object... objects) { - String invokingApiMethodName = null; - String kitManagerMethodName = null; - boolean foundInternal = false; - boolean foundExternal = false; - final List objectList = new ArrayList(); - for (Object obj : objects) { - objectList.add(obj); - } - for (int i = 0; i < stackTrace.length; i++) { - if (!isExternalApiInvocation(stackTrace[i])) { - foundInternal = true; - } - if (foundInternal && !foundExternal) { - if (isExternalApiInvocation(stackTrace[i])) { - invokingApiMethodName = getApiName(stackTrace[i - 1]); - foundExternal = true; - } - } - if (stackTrace[i].getClassName().equals("com.mparticle.kits.KitManagerImpl")) { - kitManagerMethodName = getApiName(stackTrace[i]); - } - } - final String finalInvokingApiMethodName = invokingApiMethodName; - final String finalKitManagerMethodName = kitManagerMethodName; - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onKitApiCalled(kitId, methodName, finalInvokingApiMethodName, finalKitManagerMethodName, objectList, used); - } - }); - } - - - @Override - public void onCompositeObjects(final Object child, final Object parent) { - broadcast(new SdkGraphListenerRunnable() { - @Override - public void run(GraphListener listener) { - listener.onCompositeObjects(child, parent); - } - }); - } - - @Override - public void onThreadMessage(final String handlerName, final Message msg, final boolean onNewThread) { - StackTraceElement[] stackTrace = null; - if (!onNewThread) { - stackTrace = Thread.currentThread().getStackTrace(); - } - final StackTraceElement[] finalStackTrace = stackTrace; - broadcast(new SdkGraphListenerRunnable() { - @Override - public void run(GraphListener listener) { - listener.onThreadMessage(handlerName, msg, onNewThread, finalStackTrace); - } - }); - } - - @Override - public void onEntityStored(final Long primaryKey, final String tableName, ContentValues contentValues) { - onCompositeObjects(contentValues, tableName + primaryKey); - final JSONObject jsonObject = new JSONObject(); - SdkListener.DatabaseTable table = null; - try { - table = SdkListener.DatabaseTable.valueOf(tableName.toUpperCase()); - } catch (IllegalArgumentException ex) { - table = SdkListener.DatabaseTable.UNKNOWN; - } - for (Map.Entry entry : contentValues.valueSet()) { - try { - if (entry.getValue() == null) { - jsonObject.put(entry.getKey(), JSONObject.NULL); - } else { - jsonObject.put(entry.getKey(), entry.getValue()); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - final SdkListener.DatabaseTable finalTable = table; - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onEntityStored(finalTable, primaryKey, jsonObject); - } - }); - } - - @Override - public void onNetworkRequestStarted(final SdkListener.Endpoint type, final String url, final JSONObject body, Object... objects) { - for (Object obj : objects) { - onCompositeObjects(obj, body); - } - final List objectList = new ArrayList(); - for (Object obj : objects) { - objectList.add(obj); - } - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onNetworkRequestStarted(type, url, body); - } - }); - } - - @Override - public void onNetworkRequestFinished(final SdkListener.Endpoint type, final String url, final JSONObject response, final int responseCode) { - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onNetworkRequestFinished(type, url, response, responseCode); - } - }); - } - - @Override - public void onSessionUpdated(final InternalSession internalSession) { - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onSessionUpdated(new InternalSession(internalSession)); - } - }); - } - - @Override - public void onKitDetected(final int kitId) { - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onKitDetected(kitId); - } - }); - } - - @Override - public void onKitConfigReceived(final int kitId, final String configuration) { - JSONObject jsonObject = new JSONObject(); - try { - jsonObject = new JSONObject(configuration); - } catch (JSONException e) { - } - final JSONObject jsonConfig = jsonObject; - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onKitConfigReceived(kitId, jsonConfig); - } - }); - } - - @Override - public void onKitExcluded(final int kitId, final String reason) { - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onKitExcluded(kitId, reason); - } - }); - } - - @Override - public void onKitStarted(final int kitId) { - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onKitStarted(kitId); - } - }); - } - - @Override - public void onAliasRequestFinished(final AliasResponse aliasResponse) { - broadcast(new SdkListenerRunnable() { - @Override - public void run(SdkListener listener) { - listener.onAliasRequestFinished(aliasResponse); - } - }); - } - - private void broadcast(SdkListenerRunnable runnable) { - for (WeakReference listenerRef : new ArrayList>(sdkListeners)) { - SdkListener listener = listenerRef.get(); - if (listener == null) { - sdkListeners.remove(listenerRef); - } else { - runnable.run(listener); - } - } - } - - private void broadcast(SdkGraphListenerRunnable runnable) { - for (WeakReference listenerRef : new ArrayList>(graphListeners)) { - GraphListener listener = listenerRef.get(); - if (listener == null) { - graphListeners.remove(listenerRef); - } else { - runnable.run(listener); - } - } - } - - interface SdkListenerRunnable { - void run(SdkListener listener); - } - - interface SdkGraphListenerRunnable { - void run(GraphListener listener); - } - - - private String getApiName(StackTraceElement element) { - String classNameString = getClassName(element.getClassName(), element.getMethodName()); - return getApiFormattedName(classNameString, element.getMethodName()); - } - - public static String getApiFormattedName(String className, String methodName) { - return new StringBuilder() - .append(className) - .append(".") - .append(methodName) - .append("()") - .toString(); - } - - private String getClassName(String className, String methodName) { - String[] packageNames = className.split("\\."); - String simpleClassName = packageNames[packageNames.length - 1]; - if (isObfuscated(simpleClassName)) { - try { - List superClasses = new ArrayList(); - Class clazz = Class.forName(className); // nosemgrep - superClasses.add(clazz.getSuperclass()); - for (Class interfce : clazz.getInterfaces()) { - superClasses.add(interfce); - } - for (Class superClass : superClasses) { - for (Method method : superClass.getMethods()) { - if (method.getName().equals(methodName)) { - String superClassName = getClassName(superClass.getName(), methodName); - if (!isObfuscated(superClassName)) { - return superClassName; - } - } - } - } - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } - return simpleClassName; - } - - private boolean isObfuscated(@NonNull String className) { - return Character.isLowerCase(className.toCharArray()[0]) && className.length() <= 3; - } - - private boolean isExternalApiInvocation(StackTraceElement element) { - return !element.getClassName().startsWith("com.mparticle") || - (element.getClassName().startsWith(context.getApplicationContext().getPackageName()) && - context.getApplicationContext().getPackageName().length() > 1); - } - - private boolean hasListeners() { - return instance.sdkListeners.size() > 0 || instance.graphListeners.size() > 0; - } -} diff --git a/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt b/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt index 429f0c25f..1d01f997b 100644 --- a/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt +++ b/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt @@ -392,7 +392,7 @@ open class AppStateManager @JvmOverloads constructor( session = InternalSession() val instance = MParticle.getInstance() instance?.Internal()?.kitManager?.onSessionEnd() - InternalListenerManager.getListener().onSessionUpdated(session) + InternalListenerManager.listener.onSessionUpdated(session) } private fun disableLocationTracking() { diff --git a/android-core/src/main/kotlin/com/mparticle/internal/listeners/ApiClass.kt b/android-core/src/main/kotlin/com/mparticle/internal/listeners/ApiClass.kt new file mode 100644 index 000000000..da86188a2 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/internal/listeners/ApiClass.kt @@ -0,0 +1,5 @@ +package com.mparticle.internal.listeners + +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS) +annotation class ApiClass diff --git a/android-core/src/main/kotlin/com/mparticle/internal/listeners/GraphListener.kt b/android-core/src/main/kotlin/com/mparticle/internal/listeners/GraphListener.kt new file mode 100644 index 000000000..495a722ea --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/internal/listeners/GraphListener.kt @@ -0,0 +1,14 @@ +package com.mparticle.internal.listeners + +import android.os.Message + +interface GraphListener { + fun onCompositeObjects(child: Any?, parent: Any?) + + fun onThreadMessage( + handlerName: String, + msg: Message?, + onNewThread: Boolean, + stackTrace: Array? + ) +} diff --git a/android-core/src/main/java/com/mparticle/internal/listeners/InternalListener.java b/android-core/src/main/kotlin/com/mparticle/internal/listeners/InternalListener.kt similarity index 52% rename from android-core/src/main/java/com/mparticle/internal/listeners/InternalListener.java rename to android-core/src/main/kotlin/com/mparticle/internal/listeners/InternalListener.kt index e7d546a84..0f496e212 100644 --- a/android-core/src/main/java/com/mparticle/internal/listeners/InternalListener.java +++ b/android-core/src/main/kotlin/com/mparticle/internal/listeners/InternalListener.kt @@ -1,21 +1,15 @@ -package com.mparticle.internal.listeners; - -import android.content.ContentValues; -import android.os.Build; -import android.os.Message; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import com.mparticle.SdkListener; -import com.mparticle.identity.AliasResponse; -import com.mparticle.internal.InternalSession; - -import org.json.JSONObject; - -public interface InternalListener { - +package com.mparticle.internal.listeners + +import android.content.ContentValues +import android.os.Build +import android.os.Message +import androidx.annotation.RequiresApi +import com.mparticle.SdkListener +import com.mparticle.identity.AliasResponse +import com.mparticle.internal.InternalSession +import org.json.JSONObject + +interface InternalListener { /** * This method should be called within the body of a public API method. Generally * we only want to instrument API methods which "do something", i.e., log an event or change @@ -23,7 +17,7 @@ public interface InternalListener { * * @param objects the arguments passed into the API method */ - void onApiCalled(Object... objects); + fun onApiCalled(vararg objects: Any) /** * To be called when a Kit's API method is invoked. This overloaded variant should be used when @@ -34,7 +28,7 @@ public interface InternalListener { * @param used whether the Kit's method returned ReportingMessages, or null if return type is void * @param objects the arguments supplied to the Kit */ - void onKitApiCalled(int kitId, Boolean used, Object... objects); + fun onKitApiCalled(kitId: Int, used: Boolean, vararg objects: Any) /** * to be called when a Kit's API method is invoked, and the name of the Kit's method is different @@ -45,7 +39,7 @@ public interface InternalListener { * @param used whether the Kit's method returned ReportingMessages, or null if return type is void * @param objects the arguments supplied to the Kit */ - void onKitApiCalled(String methodName, int kitId, Boolean used, Object... objects); + fun onKitApiCalled(methodName: String, kitId: Int, used: Boolean, vararg objects: Any) /** * establishes a child-parent relationship between two objects. It is not necessary to call this @@ -54,7 +48,7 @@ public interface InternalListener { * @param child the child object * @param parent the parent object */ - void onCompositeObjects(@Nullable Object child, @Nullable Object parent); + fun onCompositeObjects(child: Any?, parent: Any?) /** * denotes that an object is going to be passed to a new Thread, and is a candidate to be a "composite" @@ -64,7 +58,7 @@ public interface InternalListener { * @param handlerName the Name of the Handler class, for example "com.mparticle.internal.MessageHandler" * @param msg the Message object */ - void onThreadMessage(@NonNull String handlerName, @NonNull Message msg, boolean onNewThread); + fun onThreadMessage(handlerName: String, msg: Message, onNewThread: Boolean) /** * indicates that an entry has been stored in the Database @@ -74,7 +68,7 @@ public interface InternalListener { * @param contentValues the ContentValues object to be inserted */ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) - void onEntityStored(Long rowId, String tableName, ContentValues contentValues); + fun onEntityStored(rowId: Long, tableName: String, contentValues: ContentValues?) /** * indicates that a Network Request has been started @@ -84,7 +78,12 @@ public interface InternalListener { * @param body the request body * @param objects any underlying objects that the request body is derived from, for example, an IdentityApiRequest instance */ - void onNetworkRequestStarted(SdkListener.Endpoint type, String url, JSONObject body, Object... objects); + fun onNetworkRequestStarted( + type: SdkListener.Endpoint, + url: String, + body: JSONObject?, + vararg objects: Any + ) /** * indicates that a NetworkRequest has been finished @@ -93,7 +92,12 @@ public interface InternalListener { * @param response the response body * @param responseCode the response code */ - void onNetworkRequestFinished(SdkListener.Endpoint type, String url, JSONObject response, int responseCode); + fun onNetworkRequestFinished( + type: SdkListener.Endpoint, + url: String, + response: JSONObject?, + responseCode: Int + ) /** * this should be called when the current Session changes, for example, it starts, stops or the @@ -101,14 +105,14 @@ public interface InternalListener { * * @param internalSession */ - void onSessionUpdated(InternalSession internalSession); + fun onSessionUpdated(internalSession: InternalSession) /** * indicates that a Kit dependency is present * * @param kitId */ - void onKitDetected(int kitId); + fun onKitDetected(kitId: Int) /** * indicates that we have received a configuration for a Kit @@ -116,7 +120,7 @@ public interface InternalListener { * @param kitId * @param configuration */ - void onKitConfigReceived(int kitId, String configuration); + fun onKitConfigReceived(kitId: Int, configuration: String?) /** * indicates that a Kit was present, and a configuration was received for it, but it was not started, @@ -126,44 +130,88 @@ public interface InternalListener { * @param kitId * @param reason */ - void onKitExcluded(int kitId, String reason); + fun onKitExcluded(kitId: Int, reason: String?) /** * indicates that a Kit successfully executed it's onKitCreate() method * * @param kitId */ - void onKitStarted(int kitId); - - void onAliasRequestFinished(AliasResponse aliasResponse); - - InternalListener EMPTY = new InternalListener() { - public void onApiCalled(Object... objects) { /* stub */} - - public void onKitApiCalled(int kitId, Boolean used, Object... objects) { /* stub */} - - public void onKitApiCalled(String methodName, int kitId, Boolean used, Object... objects) { /* stub */} - - public void onEntityStored(Long rowId, String tableName, ContentValues contentValues) { /* stub */} - - public void onNetworkRequestStarted(SdkListener.Endpoint type, String url, JSONObject body, Object... objects) { /* stub */} - - public void onNetworkRequestFinished(SdkListener.Endpoint type, String url, JSONObject response, int responseCode) { /* stub */} - - public void onSessionUpdated(InternalSession internalSession) { /* stub */} - - public void onKitDetected(int kitId) { /* stub */} - - public void onKitConfigReceived(int kitId, String configuration) { /* stub */} - - public void onKitExcluded(int kitId, String reason) { /* stub */} - - public void onKitStarted(int kitId) { /* stub */} - - public void onAliasRequestFinished(AliasResponse aliasResponse) { /* stub */} - - public void onCompositeObjects(Object child, Object parent) { /* stub */} - - public void onThreadMessage(String handlerName, Message msg, boolean onNewThread) { /* stub */ } - }; + fun onKitStarted(kitId: Int) + + fun onAliasRequestFinished(aliasResponse: AliasResponse?) + + companion object { + @JvmField + val EMPTY: InternalListener = object : InternalListener { + override fun onApiCalled(vararg objects: Any) { /* stub */ + } + + override fun onKitApiCalled( + kitId: Int, + used: Boolean, + vararg objects: Any + ) { /* stub */ + } + + override fun onKitApiCalled( + methodName: String, + kitId: Int, + used: Boolean, + vararg objects: Any + ) { /* stub */ + } + + override fun onEntityStored( + rowId: Long, + tableName: String, + contentValues: ContentValues? + ) { /* stub */ + } + + override fun onNetworkRequestStarted( + type: SdkListener.Endpoint, + url: String, + body: JSONObject?, + vararg objects: Any + ) { /* stub */ + } + + override fun onNetworkRequestFinished( + type: SdkListener.Endpoint, + url: String, + response: JSONObject?, + responseCode: Int + ) { /* stub */ + } + + override fun onSessionUpdated(internalSession: InternalSession) { /* stub */ + } + + override fun onKitDetected(kitId: Int) { /* stub */ + } + + override fun onKitConfigReceived(kitId: Int, configuration: String?) { /* stub */ + } + + override fun onKitExcluded(kitId: Int, reason: String?) { /* stub */ + } + + override fun onKitStarted(kitId: Int) { /* stub */ + } + + override fun onAliasRequestFinished(aliasResponse: AliasResponse?) { /* stub */ + } + + override fun onCompositeObjects(child: Any?, parent: Any?) { /* stub */ + } + + override fun onThreadMessage( + handlerName: String, + msg: Message, + onNewThread: Boolean + ) { /* stub */ + } + } + } } diff --git a/android-core/src/main/kotlin/com/mparticle/internal/listeners/InternalListenerManager.kt b/android-core/src/main/kotlin/com/mparticle/internal/listeners/InternalListenerManager.kt new file mode 100644 index 000000000..27f95ada2 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/internal/listeners/InternalListenerManager.kt @@ -0,0 +1,353 @@ +package com.mparticle.internal.listeners + +import android.content.ContentValues +import android.content.Context +import android.os.Message +import com.mparticle.SdkListener +import com.mparticle.identity.AliasResponse +import com.mparticle.internal.InternalSession +import com.mparticle.internal.KitFrameworkWrapper +import com.mparticle.internal.MPUtility +import org.json.JSONException +import org.json.JSONObject +import java.lang.ref.WeakReference +import java.util.Locale + +class InternalListenerManager private constructor(private val context: Context) : InternalListener { + val sdkListeners: MutableList> = ArrayList() + val graphListeners: MutableList> = ArrayList() + private val thrown = false + + fun addListener(sdkListener: SdkListener) { + for (listener in sdkListeners) { + if (listener.get() === sdkListener) { + return + } + } + sdkListeners.add(WeakReference(sdkListener)) + if (sdkListener is GraphListener) { + graphListeners.add(WeakReference(sdkListener as GraphListener)) + } + } + + fun removeListener(sdkListener: SdkListener) { + for (listener in ArrayList(sdkListeners)) { + if (listener.get() === sdkListener) { + sdkListeners.remove(listener) + } + } + for (listener in ArrayList(graphListeners)) { + if (listener.get() === sdkListener) { + graphListeners.remove(listener) + } + } + } + + override fun onApiCalled(vararg objects: Any) { + } + + override fun onKitApiCalled(kitId: Int, used: Boolean, vararg objects: Any) { + val stackTrace = Thread.currentThread().stackTrace + var methodName: String? = null + for (i in stackTrace.indices) { + val element = stackTrace[i] + if (element.className == KitFrameworkWrapper::class.java.name) { + methodName = element.methodName + "()" + } + } + methodName?.let { onKitApiCalled(stackTrace, it, kitId, used, *objects) } + } + + override fun onKitApiCalled( + methodName: String, kitId: Int, used: Boolean, vararg objects: Any + ) { + onKitApiCalled(Thread.currentThread().stackTrace, methodName, kitId, used, *objects) + } + + private fun onKitApiCalled( + stackTrace: Array, + methodName: String, + kitId: Int, + used: Boolean, + vararg objects: Any + ) { + var invokingApiMethodName: String? = null + var kitManagerMethodName: String? = null + var foundInternal = false + var foundExternal = false + val objectList: MutableList = ArrayList() + for (obj in objects) { + objectList.add(obj) + } + for (i in stackTrace.indices) { + if (!isExternalApiInvocation(stackTrace[i])) { + foundInternal = true + } + if (foundInternal && !foundExternal) { + if (isExternalApiInvocation(stackTrace[i])) { + invokingApiMethodName = getApiName(stackTrace[i - 1]) + foundExternal = true + } + } + if (stackTrace[i].className == "com.mparticle.kits.KitManagerImpl") { + kitManagerMethodName = getApiName(stackTrace[i]) + } + } + val finalInvokingApiMethodName = invokingApiMethodName + val finalKitManagerMethodName = kitManagerMethodName + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onKitApiCalled( + kitId, + methodName, + finalInvokingApiMethodName, + finalKitManagerMethodName, + objectList, + used + ) + } + }) + } + + override fun onCompositeObjects(child: Any?, parent: Any?) { + broadcast(object : SdkGraphListenerRunnable { + override fun run(listener: GraphListener) { + listener.onCompositeObjects(child, parent) + } + }) + } + + override fun onThreadMessage(handlerName: String, msg: Message, onNewThread: Boolean) { + var stackTrace: Array? = null + if (!onNewThread) { + stackTrace = Thread.currentThread().stackTrace + } + val finalStackTrace = stackTrace + broadcast(object : SdkGraphListenerRunnable { + override fun run(listener: GraphListener) { + listener.onThreadMessage(handlerName, msg, onNewThread, finalStackTrace) + } + }) + } + + override fun onEntityStored( + primaryKey: Long, tableName: String, contentValues: ContentValues? + ) { + onCompositeObjects(contentValues, tableName + primaryKey) + val jsonObject = JSONObject() + var table: SdkListener.DatabaseTable? = null + table = try { + tableName.uppercase(Locale.getDefault()).let { SdkListener.DatabaseTable.valueOf(it) } + } catch (ex: IllegalArgumentException) { + SdkListener.DatabaseTable.UNKNOWN + } + if (contentValues != null) { + for ((key, value) in contentValues.valueSet()) { + try { + if (value == null) { + jsonObject.put(key, JSONObject.NULL) + } else { + jsonObject.put(key, value) + } + } catch (e: JSONException) { + e.printStackTrace() + } + } + } + val finalTable = table + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + finalTable?.let { listener.onEntityStored(it, primaryKey, jsonObject) } + } + }) + } + + override fun onNetworkRequestStarted( + type: SdkListener.Endpoint, url: String, body: JSONObject?, vararg objects: Any + ) { + for (obj in objects) { + onCompositeObjects(obj, body) + } + val objectList: MutableList = ArrayList() + for (obj in objects) { + objectList.add(obj) + } + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onNetworkRequestStarted(type, url, body ?: JSONObject()) + } + }) + } + + override fun onNetworkRequestFinished( + type: SdkListener.Endpoint, url: String, response: JSONObject?, responseCode: Int + ) { + + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onNetworkRequestFinished(type, url, response, responseCode) + } + }) + } + + override fun onSessionUpdated(internalSession: InternalSession) { + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onSessionUpdated(InternalSession(internalSession)) + } + }) + } + + override fun onKitDetected(kitId: Int) { + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onKitDetected(kitId) + } + }) + } + + override fun onKitConfigReceived(kitId: Int, configuration: String?) { + var jsonObject = JSONObject() + try { + jsonObject = JSONObject(configuration) + } catch (e: JSONException) { + } + val jsonConfig = jsonObject + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onKitConfigReceived(kitId, jsonConfig) + } + }) + } + + override fun onKitExcluded(kitId: Int, reason: String?) { + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onKitExcluded(kitId, reason) + } + }) + } + + override fun onKitStarted(kitId: Int) { + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onKitStarted(kitId) + } + }) + } + + override fun onAliasRequestFinished(aliasResponse: AliasResponse?) { + broadcast(object : SdkListenerRunnable { + override fun run(listener: SdkListener) { + listener.onAliasRequestFinished(aliasResponse) + } + }) + } + + private fun broadcast(runnable: SdkListenerRunnable) { + for (listenerRef in ArrayList(sdkListeners)) { + val listener = listenerRef.get() + if (listener == null) { + sdkListeners.remove(listenerRef) + } else { + runnable.run(listener) + } + } + } + + private fun broadcast(runnable: SdkGraphListenerRunnable) { + for (listenerRef in ArrayList(graphListeners)) { + val listener = listenerRef.get() + if (listener == null) { + graphListeners.remove(listenerRef) + } else { + runnable.run(listener) + } + } + } + + internal interface SdkListenerRunnable { + fun run(listener: SdkListener) + } + + internal interface SdkGraphListenerRunnable { + fun run(listener: GraphListener) + } + + private fun getApiName(element: StackTraceElement): String { + val classNameString = getClassName(element.className, element.methodName) + return getApiFormattedName(classNameString, element.methodName) + } + + private fun getClassName(className: String, methodName: String): String { + val packageNames = + className.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val simpleClassName = packageNames[packageNames.size - 1] + if (isObfuscated(simpleClassName)) { + try { + val superClasses: MutableList> = ArrayList() + val clazz = Class.forName(className) // nosemgrep + superClasses.add(clazz.superclass) + for (interfce in clazz.interfaces) { + superClasses.add(interfce) + } + for (superClass in superClasses) { + for (method in superClass.methods) { + if (method.name == methodName) { + val superClassName = getClassName(superClass.name, methodName) + if (!isObfuscated(superClassName)) { + return superClassName + } + } + } + } + } catch (e: ClassNotFoundException) { + e.printStackTrace() + } + } + return simpleClassName + } + + private fun isObfuscated(className: String): Boolean { + return Character.isLowerCase(className.toCharArray()[0]) && className.length <= 3 + } + + private fun isExternalApiInvocation(element: StackTraceElement): Boolean { + return !element.className.startsWith("com.mparticle") || (element.className.startsWith( + context.applicationContext.packageName + ) && context.applicationContext.packageName.length > 1) + } + + private fun hasListeners(): Boolean { + return (instance?.sdkListeners?.size ?: 0) > 0 || (instance?.graphListeners?.size ?: 0) > 0 + } + + companion object { + private var instance: InternalListenerManager? = null + private const val INTERNAL_LISTENER_PROP = "debug.mparticle.listener" + + @JvmStatic + fun start(context: Context?): InternalListenerManager? { + val canRun = MPUtility.isAppDebuggable(context) || context?.packageName == MPUtility.getProp(INTERNAL_LISTENER_PROP) + if (instance == null && context != null && canRun) { + instance = InternalListenerManager(context.applicationContext) + } + return instance + } + + @JvmStatic + val isEnabled: Boolean + get() = instance != null && instance?.hasListeners() == true + + @JvmStatic + val listener: InternalListener + get() = instance?.let { + if (isEnabled) it else InternalListener.EMPTY + } ?: InternalListener.EMPTY + + fun getApiFormattedName(className: String?, methodName: String?): String { + return StringBuilder().append(className).append(".").append(methodName).append("()") + .toString() + } + } +} diff --git a/android-core/src/test/kotlin/com/mparticle/internal/listeners/InternalListenerManagerTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/listeners/InternalListenerManagerTest.kt index 35190159c..b384c5b6e 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/listeners/InternalListenerManagerTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/listeners/InternalListenerManagerTest.kt @@ -21,30 +21,30 @@ class InternalListenerManagerTest { PowerMockito.mockStatic(MPUtility::class.java) Mockito.`when`(MPUtility.isDevEnv()).thenReturn(true) Mockito.`when`(MPUtility.getProp(Mockito.anyString())).thenReturn(mockContext.packageName) - Assert.assertNotNull(InternalListenerManager.getListener()) - Assert.assertEquals(InternalListenerManager.getListener(), InternalListener.EMPTY) - Assert.assertFalse(InternalListenerManager.isEnabled()) + Assert.assertNotNull(InternalListenerManager.listener) + Assert.assertEquals(InternalListenerManager.listener, InternalListener.EMPTY) + Assert.assertFalse(InternalListenerManager.isEnabled) mockContext.isDebuggable = true val manager = InternalListenerManager.start(mockContext) Assert.assertNotNull(manager) // Manager is started, but should still be a brick until an SdkListener is added. - Assert.assertNotNull(InternalListenerManager.getListener()) - Assert.assertEquals(InternalListenerManager.getListener(), InternalListener.EMPTY) - Assert.assertFalse(InternalListenerManager.isEnabled()) + Assert.assertNotNull(InternalListenerManager.listener) + Assert.assertEquals(InternalListenerManager.listener, InternalListener.EMPTY) + Assert.assertFalse(InternalListenerManager.isEnabled) val listener = SdkListener() manager?.addListener(listener) // Manager should now be active, since a listener was added. - Assert.assertNotNull(InternalListenerManager.getListener()) - Assert.assertNotEquals(InternalListenerManager.getListener(), InternalListener.EMPTY) - Assert.assertTrue(InternalListenerManager.isEnabled()) + Assert.assertNotNull(InternalListenerManager.listener) + Assert.assertNotEquals(InternalListenerManager.listener, InternalListener.EMPTY) + Assert.assertTrue(InternalListenerManager.isEnabled) manager?.removeListener(listener) // Manager should go back to being a brick, since it's listener was removed. - Assert.assertNotNull(InternalListenerManager.getListener()) - Assert.assertEquals(InternalListenerManager.getListener(), InternalListener.EMPTY) - Assert.assertFalse(InternalListenerManager.isEnabled()) + Assert.assertNotNull(InternalListenerManager.listener) + Assert.assertEquals(InternalListenerManager.listener, InternalListener.EMPTY) + Assert.assertFalse(InternalListenerManager.isEnabled) } @Test @@ -57,9 +57,9 @@ class InternalListenerManagerTest { val manager = InternalListenerManager.start(context) // B rick instance of InternalListenerManager should act like a brick. - Assert.assertNotNull(InternalListenerManager.getListener()) - Assert.assertEquals(InternalListenerManager.getListener(), InternalListener.EMPTY) - Assert.assertFalse(InternalListenerManager.isEnabled()) + Assert.assertNotNull(InternalListenerManager.listener) + Assert.assertEquals(InternalListenerManager.listener, InternalListener.EMPTY) + Assert.assertFalse(InternalListenerManager.isEnabled) Assert.assertNull(manager) }