diff --git a/README.md b/README.md index 95b42a5..a2cdcf3 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,4 @@ The TikTok business SDK takes the automatically/manually logged events, and repo * We may use this app event data for subsequent retargeting and dynamic product ads like we do with app event data forwarded from measurement partners. * In the near future, we will introduce our own attribution solutions and input into our optimization models based on these app events. -** For details including integration instructions, see the [TikTok Business Mobile SDK Documentation](https://ads.tiktok.com/marketing_api/docs?rid=rscv11ob9m9&id=1683138352999426). ** +** For details including integration instructions, see the [TikTok Business Mobile SDK Documentation](https://ads.tiktok.com/marketing_api/docs?rid=rscv11ob9m9&id=1683138352999426). ** \ No newline at end of file diff --git a/build.gradle b/build.gradle index 954e37a..2ebc7fe 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,8 @@ ext { versionName = VERSION_NAME minSdkVersion = 17 - targetSdkVersion = 31 - compileSdkVersion = 31 - - sourceCompatibilityVersion = JavaVersion.VERSION_1_8 - targetCompatibilityVersion = JavaVersion.VERSION_1_8 + targetSdkVersion = 33 + compileSdkVersion = 33 } task clean(type: Delete) { diff --git a/business-core/build.gradle b/business-core/build.gradle index ebb13c7..453e648 100644 --- a/business-core/build.gradle +++ b/business-core/build.gradle @@ -33,11 +33,6 @@ android { buildConfig = true } - compileOptions { - sourceCompatibility rootProject.ext.sourceCompatibilityVersion - targetCompatibility rootProject.ext.targetCompatibilityVersion - } - lintOptions { textReport true textOutput 'stdout' @@ -52,6 +47,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-process:2.3.1' implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1' implementation 'commons-codec:commons-codec:1.11' + compileOnly('com.android.billingclient:billing:6.0.0') testImplementation 'junit:junit:4.13.2' diff --git a/business-core/src/main/AndroidManifest.xml b/business-core/src/main/AndroidManifest.xml index b9fac28..645b1fa 100644 --- a/business-core/src/main/AndroidManifest.xml +++ b/business-core/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ package="com.tiktok"> + diff --git a/business-core/src/main/java/com/tiktok/TikTokBusinessSdk.java b/business-core/src/main/java/com/tiktok/TikTokBusinessSdk.java index c748514..b08e2b4 100644 --- a/business-core/src/main/java/com/tiktok/TikTokBusinessSdk.java +++ b/business-core/src/main/java/com/tiktok/TikTokBusinessSdk.java @@ -6,13 +6,20 @@ package com.tiktok; +import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_CRASH; + import android.app.Application; import android.content.Context; +import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.tiktok.appevents.*; +import com.tiktok.appevents.base.EventName; +import com.tiktok.appevents.base.TTBaseEvent; +import com.tiktok.iap.TTInAppPurchaseWrapper; +import com.tiktok.util.RegexUtil; import com.tiktok.util.TTConst; import com.tiktok.util.TTLogger; @@ -99,14 +106,18 @@ private TikTokBusinessSdk(@NonNull TTConfig ttConfig) { logger = new TTLogger(TAG, logLevel); /* no app id exception */ - if (ttConfig.appId == null) { - throw new IllegalArgumentException("app id not set"); + if (TextUtils.isEmpty(ttConfig.appId) || !RegexUtil.validateAppId(ttConfig.appId)) { + ttConfig.appId = ""; + logger.warn("Invalid App Id!"); } - if (ttConfig.ttAppId == null) { - logger.warn("ttAppId not set, but its usage is encouraged"); + if (ttConfig.ttAppId == null || !RegexUtil.validateTTAppId(ttConfig.ttAppId)) { + ttConfig.ttAppId = ""; + ttConfig.ttAppIds = new String[]{""}; + ttConfig.ttFirstAppId = new BigInteger("0"); + logger.warn("Invalid TikTok App Id!"); } - + logger.info("appId: %s, TTAppId: %s, autoIapTrack: %s", ttConfig.appId, ttConfig.ttAppId, ttConfig.autoIapTrack); config = ttConfig; networkSwitch = new AtomicBoolean(ttConfig.autoStart); sdkDebugModeSwitch.set(ttConfig.debugModeSwitch); @@ -136,7 +147,7 @@ public static synchronized void initializeSdk(TTConfig ttConfig) { @Override public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { if (TTCrashHandler.isTTSDKRelatedException(throwable)) { - TTCrashHandler.handleCrash(TAG, throwable); + TTCrashHandler.handleCrash(TAG, throwable, TTSDK_EXCEPTION_CRASH); } if (getCrashListener() != null) { getCrashListener().onCrash(thread, throwable); @@ -155,6 +166,9 @@ public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwab // the appEventLogger instance will be the main interface to track events appEventLogger = new TTAppEventLogger(ttConfig.autoEvent,ttConfig.disabledEvents, ttConfig.flushTime, ttConfig.disableMetrics, initTimeMS); + if (ttConfig.autoIapTrack) { + TTInAppPurchaseWrapper.registerIapTrack(); + } try { long endTimeMS = System.currentTimeMillis(); @@ -258,10 +272,27 @@ void onNetworkChange(int toBeSentRequests, int successfulRequest, int failedRequ * A shortcut method for the situations where the events do not require a property body. * see more {@link TikTokBusinessSdk#trackEvent(String, JSONObject)} */ + @Deprecated public static void trackEvent(String event) { appEventLogger.track(event, null); } + @Deprecated + public static void trackEvent(String event, String eventId) { + appEventLogger.track(event, null, eventId); + } + + public static void trackTTEvent(TTBaseEvent event) { + appEventLogger.track(event.eventName, event.properties, event.eventId); + } + + public static void trackTTEvent(EventName event) { + appEventLogger.track(event.toString(), null); + } + + public static void trackTTEvent(EventName event, String eventId) { + appEventLogger.track(event.toString(), null, eventId); + } /** *
@@ -286,8 +317,13 @@ public static void trackEvent(String event) {
      *
      * @param event event name
      */
+    @Deprecated
     public static void trackEvent(String event, @Nullable JSONObject props) {
-        appEventLogger.track(event, props);
+        appEventLogger.track(event, props, "");
+    }
+    @Deprecated
+    public static void trackEvent(String event, @Nullable JSONObject props, String eventId) {
+        appEventLogger.track(event, props, eventId);
     }
 
     /**
@@ -363,10 +399,18 @@ public static String getAppId() {
     /**
      * returns api_id
      */
-    public static BigInteger getTTAppId() {
+    public static String getTTAppId() {
         return config.ttAppId;
     }
 
+    public static String[] getTTAppIds() {
+        return config.ttAppIds;
+    }
+
+    public static BigInteger getFirstTTAppIds() {
+        return config.ttFirstAppId;
+    }
+
     /**
      * if only appId is provided, the json schema would be like
      * {
@@ -416,7 +460,7 @@ public static BigInteger getTTAppId() {
 
     public static boolean onlyAppIdProvided() {
         if(config.appId == null){
-            throw new IllegalStateException("AppId should be checked before, this path should not be accessed");
+            logger.warn("AppId should be checked before, this path should not be accessed");
         }
         return config.ttAppId == null;
     }
@@ -504,7 +548,10 @@ public static synchronized void identify(String externalId,
                                              @Nullable String phoneNumber,
                                              @Nullable String email) {
         long initTimeMS = System.currentTimeMillis();
-        appEventLogger.identify(externalId, externalUserName, phoneNumber, email);
+        boolean isReset = appEventLogger.identify(externalId, externalUserName, phoneNumber, email);
+        if (!isReset) {
+            return;
+        }
         try {
             long endTimeMS = System.currentTimeMillis();
             JSONObject meta = TTUtil.getMetaWithTS(initTimeMS)
@@ -566,9 +613,11 @@ public static class TTConfig {
         /* application context */
         private final Application application;
         /* api_id for api calls, App ID in EM */
-        private String appId;
+        private String appId = "";
         /* tt_app_id for api calls, TikTok App ID from EM */
-        private BigInteger ttAppId;
+        private String ttAppId = "";
+        private String[] ttAppIds = {""};
+        private BigInteger ttFirstAppId=new BigInteger("0");
         /* flush time interval in seconds, default 15, 0 -> disabled */
         private int flushTime = 15;
 //        /* Access-Token for api calls */
@@ -590,6 +639,8 @@ public static class TTConfig {
         /* open LDU mode*/
         private boolean lduModeSwitch = false;
 
+        private boolean autoIapTrack = false;
+
 
         /**
          * Read configs from 
@@ -614,7 +665,12 @@ public TTConfig setLogLevel(LogLevel ll) {
          * set app id
          */
         public TTConfig setTTAppId(String ttAppId) {
-            this.ttAppId = new BigInteger(ttAppId);
+            this.ttAppId = ttAppId;
+            try {
+                ttAppIds = (ttAppId.replace(" ", "")).split(",");
+                ttFirstAppId = new BigInteger(ttAppIds[0]);
+            } catch (Throwable throwable) {
+            }
             return this;
         }
 
@@ -622,7 +678,9 @@ public TTConfig setTTAppId(String ttAppId) {
          * set app id
          */
         public TTConfig setAppId(String apiId) {
-            this.appId = apiId;
+            if (!TextUtils.isEmpty(apiId)) {
+                this.appId = apiId;
+            }
             return this;
         }
 
@@ -706,6 +764,18 @@ public  TTConfig enableLimitedDataUse() {
             lduModeSwitch = true;
             return this;
         }
+
+        /**
+         * to open the Auto In App Purchase Track
+         */
+        public  TTConfig enableAutoIapTrack() {
+            autoIapTrack = true;
+            return this;
+        }
+
+        public boolean isAutoIapTrack() {
+            return autoIapTrack;
+        }
     }
 
     /**
@@ -725,6 +795,10 @@ public boolean log() {
         }
     }
 
+    public static boolean enableAutoIapTrack() {
+        return config != null && config.isAutoIapTrack();
+    }
+
     public interface CrashListener {
         void onCrash(Thread thread, Throwable ex);
     }
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTActivityLifecycleCallbacksListener.java b/business-core/src/main/java/com/tiktok/appevents/TTActivityLifecycleCallbacksListener.java
index e75ef77..9bfaa53 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTActivityLifecycleCallbacksListener.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTActivityLifecycleCallbacksListener.java
@@ -6,10 +6,13 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.TikTokBusinessSdk.enableAutoIapTrack;
+
 import androidx.annotation.NonNull;
 import androidx.lifecycle.LifecycleOwner;
 
 import com.tiktok.TikTokBusinessSdk;
+import com.tiktok.iap.TTInAppPurchaseWrapper;
 import com.tiktok.util.TTLogger;
 import com.tiktok.util.TTUtil;
 import org.json.JSONObject;
@@ -47,6 +50,9 @@ public void onPause(@NonNull LifecycleOwner owner) {
         bgStart = System.currentTimeMillis();
         appEventLogger.stopScheduler();
         isPaused = true;
+        if(enableAutoIapTrack()) {
+            TTInAppPurchaseWrapper.startBillingClient();
+        }
     }
 
     @Override
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTAppEvent.java b/business-core/src/main/java/com/tiktok/appevents/TTAppEvent.java
index bf0694f..0569f3c 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTAppEvent.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTAppEvent.java
@@ -10,7 +10,9 @@
 import com.tiktok.util.TTLogger;
 
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
 
 public class TTAppEvent implements Serializable {
@@ -20,29 +22,36 @@ public static enum TTAppEventType{
         identify
     }
 
-    private static final long serialVersionUID = 1L;
-
+    private static final long serialVersionUID = 2L;
+    private List tiktokAppIds = new ArrayList<>();
     private TTAppEventType type;
     private String eventName;
     private Date timeStamp;
     private String propertiesJson;
+    private String eventId;
     private static AtomicLong counter = new AtomicLong(new Date().getTime() + 0L);
     private Long uniqueId;
     private TTUserInfo userInfo;
     private static String TAG = TTAppEventsQueue.class.getCanonicalName();
     private static TTLogger logger = new TTLogger(TAG, TikTokBusinessSdk.getLogLevel());
 
-    TTAppEvent(TTAppEventType type, String eventName, String propertiesJson) {
-        this(type, eventName, new Date(), propertiesJson);
+    TTAppEvent(TTAppEventType type, String eventName, String propertiesJson, String eventId, String[] ttAppId) {
+        this(type, eventName, new Date(), propertiesJson, eventId, ttAppId);
     }
 
-    TTAppEvent(TTAppEventType type, String eventName, Date timeStamp, String propertiesJson) {
+    TTAppEvent(TTAppEventType type, String eventName, Date timeStamp, String propertiesJson, String eventId, String[] ttAppId) {
         this.type = type;
         this.eventName = eventName;
         this.timeStamp = timeStamp;
         this.propertiesJson = propertiesJson;
+        this.eventId = eventId;
         this.uniqueId = TTAppEvent.counter.getAndIncrement();
         this.userInfo = TTUserInfo.sharedInstance.clone();
+        if (ttAppId != null && ttAppId.length > 0) {
+            for (int i = 0; i < ttAppId.length; i++) {
+                tiktokAppIds.add(ttAppId[i]);
+            }
+        }
     }
 
     public TTUserInfo getUserInfo() {
@@ -69,6 +78,14 @@ public void setTimeStamp(Date timeStamp) {
         this.timeStamp = timeStamp;
     }
 
+    public String getEventId() {
+        return eventId;
+    }
+
+    public void setEventId(String eventId) {
+        this.eventId = eventId;
+    }
+
     public String getPropertiesJson() {
         return propertiesJson;
     }
@@ -81,13 +98,23 @@ public Long getUniqueId() {
         return this.uniqueId;
     }
 
+    public List getTiktokAppIds() {
+        return tiktokAppIds;
+    }
+
+    public void setTiktokAppIds(List tiktokAppIds) {
+        this.tiktokAppIds = tiktokAppIds;
+    }
+
     @Override
     public String toString() {
         return "TTAppEvent{" +
                 "eventName='" + eventName + '\'' +
                 ", timeStamp=" + timeStamp +
                 ", propertiesJson='" + propertiesJson + '\'' +
+                ", eventId='" + eventId + '\'' +
                 ", uniqueId=" + uniqueId +
+                ", tiktokAppIds=" + tiktokAppIds +
                 '}';
     }
 }
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTAppEventLogger.java b/business-core/src/main/java/com/tiktok/appevents/TTAppEventLogger.java
index a0443ec..b2c99ed 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTAppEventLogger.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTAppEventLogger.java
@@ -6,6 +6,10 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
+
+import android.text.TextUtils;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.lifecycle.Lifecycle;
@@ -119,7 +123,7 @@ public void trackPurchase(List purchaseInfos) {
             for (TTPurchaseInfo purchaseInfo : purchaseInfos) {
                 JSONObject property = TTInAppPurchaseManager.getPurchaseProps(purchaseInfo);
                 if (property != null) {
-                    track("Purchase", property);
+                    track("Purchase", property, purchaseInfo.getEventId());
                 }
             }
         });
@@ -171,7 +175,7 @@ void stopScheduler() {
         }
     }
 
-    public void identify(String externalId,
+    public boolean identify(String externalId,
                          @Nullable String externalUserName,
                          @Nullable String phoneNumber,
                          @Nullable String email) {
@@ -179,15 +183,16 @@ public void identify(String externalId,
         if (sharedInstance.isIdentified()) {
             logger.warn("SDK is already identified, if you want to switch to another" +
                     "user account, plz call TiktokBusinessSDK.logout() first and then identify");
-            return;
+            return false;
         }
         sharedInstance.setIdentified();
         sharedInstance.setExternalId(externalId);
         sharedInstance.setExternalUserName(externalUserName);
         sharedInstance.setPhoneNumber(phoneNumber);
         sharedInstance.setEmail(email);
-        trackEvent(TTAppEvent.TTAppEventType.identify, null, null);
+        trackEvent(TTAppEvent.TTAppEventType.identify, null, null, null);
         flushWithReason(TTAppEventLogger.FlushReason.IDENTIFY);
+        return true;
     }
 
     public void logout() {
@@ -202,11 +207,14 @@ public void logout() {
      * @param props
      */
     public void track(String event, @Nullable JSONObject props) {
-        trackEvent(TTAppEvent.TTAppEventType.track, event, props);
+        trackEvent(TTAppEvent.TTAppEventType.track, event, props, null);
+    }
+    public void track(String event, @Nullable JSONObject props, String eventId) {
+        trackEvent(TTAppEvent.TTAppEventType.track, event, props, eventId);
     }
 
-    private void trackEvent(TTAppEvent.TTAppEventType type, String event, @Nullable JSONObject props) {
-        if (!TikTokBusinessSdk.isSystemActivated()) {
+    private void trackEvent(TTAppEvent.TTAppEventType type, String event, @Nullable JSONObject props, String eventId) {
+        if (!TikTokBusinessSdk.isSystemActivated() || TextUtils.isEmpty(TikTokBusinessSdk.getAppId())) {
             return;
         }
 
@@ -216,7 +224,7 @@ private void trackEvent(TTAppEvent.TTAppEventType type, String event, @Nullable
                 logger.debug("track " + event + " : " + finalProps.toString(4));
             } catch (JSONException ignored) {}
 
-            TTAppEventsQueue.addEvent(new TTAppEvent(type, event, finalProps.toString()));
+            TTAppEventsQueue.addEvent(new TTAppEvent(type, event, finalProps.toString(), eventId, TikTokBusinessSdk.getTTAppIds()));
 
             if (TTAppEventsQueue.size() > THRESHOLD) {
                 flush(FlushReason.THRESHOLD);
@@ -285,7 +293,7 @@ void flush(FlushReason reason) {
                 TTAppEventStorage.persist(null);
             }
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
         }
 
         if (flushSize != 0) {
@@ -325,7 +333,7 @@ private void addToQ(Runnable task) {
         try {
             eventLoop.execute(task);
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
         }
     }
 
@@ -336,7 +344,7 @@ private void addToLater(Runnable task, int seconds) {
         try {
             eventLoop.schedule(task, seconds, TimeUnit.SECONDS);
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
         }
     }
 
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTAppEventStorage.java b/business-core/src/main/java/com/tiktok/appevents/TTAppEventStorage.java
index f825be8..16a584c 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTAppEventStorage.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTAppEventStorage.java
@@ -6,6 +6,8 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
+
 import android.content.Context;
 
 import com.tiktok.TikTokBusinessSdk;
@@ -13,10 +15,9 @@
 import com.tiktok.util.TTUtil;
 import org.json.JSONObject;
 
-import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.ObjectInputStream;
+import java.io.FileInputStream;
 import java.io.ObjectOutputStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -62,7 +63,7 @@ public synchronized static void persist(List failedEvents) {
         toBeSaved.addEvents(eventsFromDisk.getAppEvents());
         toBeSaved.addEvents(eventsFromMemory);
 
-        //If end up persisting more than 10,000 events, persist the latest 10,000 events by timestamp
+        //If end up persisting more than 500 events, persist the latest 500 events by timestamp
         discardOldEvents(toBeSaved, MAX_PERSIST_EVENTS_NUM);
         saveToDisk(toBeSaved);
     }
@@ -111,7 +112,7 @@ private static boolean saveToDisk(TTAppEventPersist appEventPersist) {
             }
             success = true;
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
         }
         try {
             long endTimeMS = System.currentTimeMillis();
@@ -142,9 +143,8 @@ synchronized static TTAppEventPersist readFromDisk() {
 
         TTAppEventPersist appEventPersist = new TTAppEventPersist();
 
-        try (ObjectInputStream ois = new ObjectInputStream(
-                new BufferedInputStream(context.openFileInput(EVENT_STORAGE_FILE)))) {
-            appEventPersist = (TTAppEventPersist) ois.readObject();
+        try (FileInputStream ois = context.openFileInput(EVENT_STORAGE_FILE)) {
+            appEventPersist = TTSafeReadObjectUtil.safeReadTTAppEventPersist(ois);
             logger.debug("disk read data: %s", appEventPersist);
             deleteFile(f);
             if (TikTokBusinessSdk.diskListener != null) {
@@ -152,7 +152,7 @@ synchronized static TTAppEventPersist readFromDisk() {
             }
         } catch (Exception e) {
             deleteFile(f);
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
         }
 
         try {
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTCrashHandler.java b/business-core/src/main/java/com/tiktok/appevents/TTCrashHandler.java
index c069a02..ec500ea 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTCrashHandler.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTCrashHandler.java
@@ -38,10 +38,10 @@ public class TTCrashHandler {
 
     static TTCrashReport crashReport = new TTCrashReport();
 
-    public static void handleCrash(String originTag, Throwable ex) {
+    public static void handleCrash(String originTag, Throwable ex, int type) {
         ttLogger.error(ex, "Error caused by sdk at " + originTag +
                 "\n" + ex.getMessage() + "\n" + getStackTrace(ex));
-        persistException(ex);
+        persistException(ex, type);
     }
 
     public static void retryLater(JSONObject monitor) {
@@ -119,11 +119,11 @@ public void addReport(String o, long t, int a) {
         }
     }
 
-    private static void persistException(Throwable ex) {
+    private static void persistException(Throwable ex, int type) {
         JSONObject stat = null;
         try {
             stat = TTRequestBuilder.getHealthMonitorBase();
-            JSONObject monitor = TTUtil.getMonitorException(ex, null);
+            JSONObject monitor = TTUtil.getMonitorException(ex, null, type);
             stat.put("monitor", monitor);
             crashReport.addReport(stat.toString(), System.currentTimeMillis(), 0);
             saveToFile(crashReport);
@@ -161,9 +161,7 @@ private static TTCrashReport readFromFile() {
         Context context = TikTokBusinessSdk.getApplicationContext();
         try {
             FileInputStream fis = context.openFileInput(CRASH_REPORT_FILE);
-            ObjectInputStream is = new ObjectInputStream(fis);
-            meta = (TTCrashReport) is.readObject();
-            is.close();
+            meta = TTSafeReadObjectUtil.safeReadTTCrashHandler(fis);
             fis.close();
         } catch (Exception ignored) {}
         return meta;
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTInAppPurchaseManager.java b/business-core/src/main/java/com/tiktok/appevents/TTInAppPurchaseManager.java
index 167150f..6814ffb 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTInAppPurchaseManager.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTInAppPurchaseManager.java
@@ -6,6 +6,8 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -26,7 +28,7 @@ static JSONObject getPurchaseProps(TTPurchaseInfo purchaseInfo) {
             JSONObject skuDetail = purchaseInfo.getSkuDetails();
             return getPurchaseProperties(productId, skuDetail);
         } catch (JSONException e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
             return null;
         }
     }
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTPurchaseInfo.java b/business-core/src/main/java/com/tiktok/appevents/TTPurchaseInfo.java
index 559f8e5..cb0373c 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTPurchaseInfo.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTPurchaseInfo.java
@@ -11,6 +11,7 @@
 public class TTPurchaseInfo {
     private final JSONObject purchase;
     private final JSONObject skuDetails;
+    private String eventId;
 
     public static class InvalidTTPurchaseInfoException extends Exception {
 
@@ -41,6 +42,11 @@ public TTPurchaseInfo(JSONObject purchase, JSONObject skuDetails) throws Invalid
         this.skuDetails = skuDetails;
     }
 
+    public TTPurchaseInfo(JSONObject purchase, JSONObject skuDetails, String eventId) throws InvalidTTPurchaseInfoException {
+        this(purchase, skuDetails);
+        this.eventId = eventId;
+    }
+
     public JSONObject getPurchase() {
         return purchase;
     }
@@ -82,4 +88,8 @@ private boolean isValidSkuDetails(JSONObject skuDetails) {
         return !skuDetails.isNull("price")
                 && !skuDetails.isNull("productId");
     }
+
+    public String getEventId() {
+        return eventId;
+    }
 }
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTRequest.java b/business-core/src/main/java/com/tiktok/appevents/TTRequest.java
index 4193da8..4cb2564 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTRequest.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTRequest.java
@@ -6,6 +6,10 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
+
+import android.text.TextUtils;
+
 import com.tiktok.BuildConfig;
 import com.tiktok.TikTokBusinessSdk;
 import com.tiktok.util.HttpRequestUtil;
@@ -71,10 +75,10 @@ public static JSONObject getBusinessSDKConfig(Map options) {
         //  for fix bug in lower Android API edition. Maybe there is something wrong with language package, url can not be parsed successfully with some special char
         //  paramsMap.put("app_name", SystemInfoUtil.getAppName());
         paramsMap.put("app_version", SystemInfoUtil.getAppVersionName());
-        paramsMap.put("tiktok_app_id", TikTokBusinessSdk.getTTAppId());
+        paramsMap.put("tiktok_app_id", TikTokBusinessSdk.getFirstTTAppIds());
         paramsMap.putAll(options);
 
-        String url = "https://business-api.tiktok.com/open_api/business_sdk_config/get/?" + TTUtil.mapToString(paramsMap, "&");
+        String url = TTUtil.mapToString("https://business-api.tiktok.com/open_api/business_sdk_config/get/", paramsMap);
         logger.debug(url);
         String result = HttpRequestUtil.doGet(url, getHeadParamMap);
         logger.debug(result);
@@ -89,7 +93,7 @@ public static JSONObject getBusinessSDKConfig(Map options) {
                 logger.info("Global config fetched: " + TTUtil.ppStr(config));
             } catch (Exception e) {
                 // might be api returning something wrong
-                TTCrashHandler.handleCrash(TAG, e);
+                TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
             }
         }
         try {
@@ -162,7 +166,7 @@ public static synchronized List reportAppEvent(JSONObject basePayloa
                 bodyJson.put("batch", new JSONArray(batch));
             } catch (Exception e) {
                 failedEventsToBeSaved.addAll(currentBatch);
-                TTCrashHandler.handleCrash(TAG, e);
+                TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
                 continue;
             }
 
@@ -207,7 +211,7 @@ else if (code == TTConst.ApiErrorCodes.PARTIAL_SUCCESS.code) {
                                 }
                             }
                         } catch (Exception e) {
-                            TTCrashHandler.handleCrash(TAG, e);
+                            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
                             failedEventsToBeSaved.addAll(currentBatch);
                             failedRequests += currentBatch.size();
                         }
@@ -221,7 +225,7 @@ else if (code == TTConst.ApiErrorCodes.PARTIAL_SUCCESS.code) {
                 } catch (JSONException e) {
                     failedRequests += currentBatch.size();
                     failedEventsToBeSaved.addAll(currentBatch);
-                    TTCrashHandler.handleCrash(TAG, e);
+                    TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
                 }
                 logger.debug(TTUtil.ppStr(result));
             }
@@ -280,6 +284,9 @@ private static JSONObject transferJson(TTAppEvent event) {
             if (event.getEventName() != null) {
                 propertiesJson.put("event", event.getEventName());
             }
+            if (!TextUtils.isEmpty(event.getEventId())) {
+                propertiesJson.put("event_id", event.getEventId());
+            }
             propertiesJson.put("timestamp", TimeUtil.getISO8601Timestamp(event.getTimeStamp()));
             if (TikTokBusinessSdk.isInSdkLDUMode()) {
                 propertiesJson.put("limited_data_use", true);
@@ -291,7 +298,7 @@ private static JSONObject transferJson(TTAppEvent event) {
             propertiesJson.put("context", TTRequestBuilder.getContextForApi(event));
             return propertiesJson;
         } catch (JSONException e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
             return null;
         }
     }
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTRequestBuilder.java b/business-core/src/main/java/com/tiktok/appevents/TTRequestBuilder.java
index c6629ea..8d7160f 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTRequestBuilder.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTRequestBuilder.java
@@ -6,6 +6,8 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
+
 import android.content.Context;
 import android.os.Build;
 
@@ -16,6 +18,7 @@
 import com.tiktok.util.SystemInfoUtil;
 import com.tiktok.util.TTUtil;
 
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -38,14 +41,14 @@ public static JSONObject getBasePayload() {
             if (TikTokBusinessSdk.onlyAppIdProvided()) {// to be compatible with the old versions
                 result.put("app_id", TikTokBusinessSdk.getAppId());
             } else {
-                result.put("tiktok_app_id", TikTokBusinessSdk.getTTAppId());
+                result.put("tiktok_app_id", TikTokBusinessSdk.getFirstTTAppIds());
             }
             if (TikTokBusinessSdk.isInSdkDebugMode()) {
-                result.put("test_event_code", TikTokBusinessSdk.getTestEventCode());
+                result.put("test_event_code", String.valueOf(TikTokBusinessSdk.getFirstTTAppIds()));
             }
             result.put("event_source", "APP_EVENTS_SDK");
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
             basePayloadCache = new JSONObject();
             return basePayloadCache;
         }
@@ -56,8 +59,10 @@ public static JSONObject getBasePayload() {
     private static JSONObject contextForApiCache = null;
 
     // the context part that does not change
-    private static JSONObject getImmutableContextForApi() throws JSONException {
+    private static JSONObject getImmutableContextForApi(TTAppEvent event) throws JSONException {
         if (contextForApiCache != null) {
+            freshTTAppID(contextForApiCache, event);
+            freshOsVersion(contextForApiCache, event);
             return contextForApiCache;
         }
         TTIdentifierFactory.AdIdInfo adIdInfo = null;
@@ -75,11 +80,49 @@ private static JSONObject getImmutableContextForApi() throws JSONException {
             TikTokBusinessSdk.getAppEventLogger().monitorMetric("did_end", meta, null);
         } catch (Exception ignored) {}
         contextForApiCache = contextBuilder(adIdInfo);
+        freshTTAppID(contextForApiCache, event);
+        freshOsVersion(contextForApiCache, event);
         return contextForApiCache;
     }
 
+    private static void freshTTAppID(JSONObject contextForApiCache, TTAppEvent event) {
+        try {
+            JSONObject app = contextForApiCache.getJSONObject("app");
+            if (event != null && app != null) {
+                JSONArray tiktokAppIds = new JSONArray();
+                if (event.getTiktokAppIds() != null) {
+                    for (int i = 0; i < event.getTiktokAppIds().size(); i++) {
+                        tiktokAppIds.put(event.getTiktokAppIds().get(i));
+                    }
+                    app.remove("tiktok_app_id");
+                    app.put("tiktok_app_ids", tiktokAppIds);
+                }
+            } else {
+                app.remove("tiktok_app_ids");
+                app.put("tiktok_app_id", TikTokBusinessSdk.getTTAppId());
+            }
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+        }
+    }
+
+    private static void freshOsVersion(JSONObject contextForApiCache, TTAppEvent event) {
+        try {
+            JSONObject device = contextForApiCache.getJSONObject("device");
+            if (event != null && device != null) {
+                device.put("os_version", SystemInfoUtil.getAndroidVersion());
+                device.remove("version");
+            } else {
+                device.put("version", SystemInfoUtil.getAndroidVersion());
+                device.remove("os_version");
+            }
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+        }
+    }
+
     public static JSONObject getContextForApi(TTAppEvent event) throws JSONException {
-        JSONObject immutablePart = getImmutableContextForApi();
+        JSONObject immutablePart = getImmutableContextForApi(event);
         JSONObject finalObj = new JSONObject(immutablePart.toString());
         finalObj.put("user", event.getUserInfo().toJsonObject());
         return finalObj;
@@ -159,7 +202,6 @@ private static JSONObject contextBuilder(@Nullable TTIdentifierFactory.AdIdInfo
 
         JSONObject device = new JSONObject();
         device.put("platform", "Android");
-        device.put("version", SystemInfoUtil.getAndroidVersion());
         if (adIdInfo != null) {
             device.put("gaid", adIdInfo.getAdId());
         }
@@ -201,11 +243,11 @@ public static JSONObject getHealthMonitorBase() throws JSONException {
             return healthBasePayloadCache;
         }
         JSONObject finalObj = new JSONObject();
-        JSONObject app = new JSONObject(getImmutableContextForApi().getJSONObject("app").toString());
+        JSONObject app = new JSONObject(getImmutableContextForApi(null).getJSONObject("app").toString());
         app.put("app_namespace", SystemInfoUtil.getPackageName());
         finalObj.put("app", app);
-        finalObj.put("library", getImmutableContextForApi().get("library"));
-        finalObj.put("device", enrichDeviceBase(getImmutableContextForApi().getJSONObject("device")));
+        finalObj.put("library", getImmutableContextForApi(null).get("library"));
+        finalObj.put("device", enrichDeviceBase(getImmutableContextForApi(null).getJSONObject("device")));
         finalObj.put("log_extra", null);
         healthBasePayloadCache = finalObj;
         return healthBasePayloadCache;
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTSafeReadObjectUtil.java b/business-core/src/main/java/com/tiktok/appevents/TTSafeReadObjectUtil.java
new file mode 100644
index 0000000..236648f
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/TTSafeReadObjectUtil.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * Copyright (c) 2024. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+package com.tiktok.appevents;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class TTSafeReadObjectUtil {
+    /**
+     * Safely read the object and do some security checks
+     */
+
+    public static TTAppEventPersist safeReadTTAppEventPersist(InputStream in) throws IOException, ClassNotFoundException {
+        List> safeClasses = new ArrayList<>();
+        safeClasses.add(TTAppEventPersist.class);
+        safeClasses.add(ArrayList.class);
+        safeClasses.add(TTAppEvent.class);
+        safeClasses.add(Enum.class);
+        safeClasses.add(String.class);
+        safeClasses.add(Date.class);
+        safeClasses.add(Long.class);
+        safeClasses.add(TTUserInfo.class);
+        safeClasses.add(TTAppEvent.TTAppEventType.class);
+        return safeReadObjects(safeClasses, Long.MAX_VALUE, Long.MAX_VALUE, in);
+    }
+
+    public static TTCrashHandler.TTCrashReport safeReadTTCrashHandler(InputStream in) throws IOException, ClassNotFoundException {
+        List> safeClasses = new ArrayList<>();
+        safeClasses.add(TTCrashHandler.TTCrashReport.class);
+        safeClasses.add(TTCrashHandler.TTCrashReport.Monitor.class);
+        safeClasses.add(String.class);
+        safeClasses.add(Long.class);
+        safeClasses.add(Integer.class);
+        safeClasses.add(ArrayList.class);
+        return safeReadObjects(safeClasses, Long.MAX_VALUE, Long.MAX_VALUE, in);
+    }
+
+    public static  T safeReadObjects(List> safeClasses, long maxObjects, long maxBytes, InputStream in) throws IOException, ClassNotFoundException {
+        InputStream lis = new FilterInputStream(in) {
+            private long length = 0;
+
+            public int read() throws IOException {
+                int count = super.read();
+                if (count != -1) {
+                    length++;
+                    checkLength();
+                }
+                return count;
+            }
+
+            public int read(byte[] b, int off, int readLength) throws IOException {
+                int count = super.read(b, off, readLength);
+                if (count > 0) {
+                    length += count;
+                    checkLength();
+                }
+                return count;
+            }
+
+            private void checkLength() {
+                if (length > maxBytes) {
+                    throw new SecurityException("too many bytes from stream. Limit is " + maxBytes);
+                }
+            }
+        };
+        ObjectInputStream ois = new ObjectInputStream(lis) {
+            private int objCount = 0;
+            boolean enableResolve = enableResolveObject(true);
+
+            protected Object resolveObject(Object obj) throws IOException {
+                if (objCount++ > maxObjects)
+                    throw new SecurityException("too many objects from stream. Limit is " + maxObjects);
+                Object object = super.resolveObject(obj);
+                return object;
+            }
+
+            protected Class resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
+                Class clazz = super.resolveClass(osc);
+                if (clazz.isArray() || clazz.equals(String.class) || Number.class.isAssignableFrom(clazz) || safeClasses.contains(clazz))
+                    return clazz;
+                throw new SecurityException("deserialize unauthorized " + clazz);
+            }
+        };
+        T t = (T) ois.readObject();
+        try{
+            in.close();
+            lis.close();
+            ois.close();
+        }catch (Throwable throwable){
+            throwable.printStackTrace();
+        }
+        return t;
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTThreadFactory.java b/business-core/src/main/java/com/tiktok/appevents/TTThreadFactory.java
index bb959a8..f78c9fa 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTThreadFactory.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTThreadFactory.java
@@ -6,6 +6,8 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_CRASH;
+
 import androidx.annotation.NonNull;
 import com.tiktok.TikTokBusinessSdk;
 
@@ -20,7 +22,7 @@ public Thread newThread(Runnable r) {
         t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
             @Override
             public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
-                TTCrashHandler.handleCrash(TAG, throwable);
+                TTCrashHandler.handleCrash(TAG, throwable, TTSDK_EXCEPTION_CRASH);
                 if (TikTokBusinessSdk.getCrashListener() != null) {
                     TikTokBusinessSdk.getCrashListener().onCrash(thread, throwable);
                 }
diff --git a/business-core/src/main/java/com/tiktok/appevents/TTUserInfo.java b/business-core/src/main/java/com/tiktok/appevents/TTUserInfo.java
index 19fd70f..ce7b268 100644
--- a/business-core/src/main/java/com/tiktok/appevents/TTUserInfo.java
+++ b/business-core/src/main/java/com/tiktok/appevents/TTUserInfo.java
@@ -6,6 +6,8 @@
 
 package com.tiktok.appevents;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
+
 import android.content.Context;
 
 import com.tiktok.util.TTUtil;
@@ -49,7 +51,7 @@ private String toSha256(String str) {
             }
             return result.toString();
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
         }
         return null;
     }
@@ -95,7 +97,7 @@ public JSONObject toJsonObject() {
                 jsonObject.put("email", email);
             }
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_SDK_CATCH);
         }
         return jsonObject;
     }
diff --git a/business-core/src/main/java/com/tiktok/appevents/base/EventName.java b/business-core/src/main/java/com/tiktok/appevents/base/EventName.java
new file mode 100644
index 0000000..69790f4
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/base/EventName.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.base;
+
+public enum EventName {
+    ACHIEVE_LEVEL("AchieveLevel"),
+    ADD_PAYMENT_INFO("AddPaymentInfo"),
+    COMPLETE_TUTORIAL("CompleteTutorial"),
+    CREATE_GROUP("CreateGroup"),
+    CREATE_ROLE("CreateRole"),
+    GENERATE_LEAD("GenerateLead"),
+    IN_APP_AD_CLICK("InAppADClick"),
+    IN_APP_AD_IMPR("InAppAdImpr"),
+    INSTALL_APP("InstallApp"),
+    JOIN_GROUP("JoinGroup"),
+    LAUNCH_APP("LaunchAPP"),
+    LOAN_APPLICATION("LoanApplication"),
+    LOAN_APPROVAL("LoanApproval"),
+    LOAN_DISBURSAL("LoanDisbursal"),
+    LOGIN("Login"),
+    RATE("Rate"),
+    REGISTRATION("Registration"),
+    SEARCH("Search"),
+    SPEND_CREDITS("SpendCredits"),
+    START_TRIAL("StartTrial"),
+    SUBSCRIBE("Subscribe"),
+    UNLOCK_ACHIEVEMENT("UnlockAchievement");
+    private String eventName;
+
+    EventName(String eventName) {
+        this.eventName = eventName;
+    }
+
+    @Override
+    public String toString() {
+        return eventName;
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/base/TTBaseEvent.java b/business-core/src/main/java/com/tiktok/appevents/base/TTBaseEvent.java
new file mode 100644
index 0000000..992d8a6
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/base/TTBaseEvent.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.base;
+
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class TTBaseEvent {
+    public JSONObject properties;
+    public String eventName;
+    public String eventId;
+
+    public TTBaseEvent(String eventName, JSONObject properties, String eventId) {
+        this.eventName = eventName;
+        this.properties = properties;
+        this.eventId = eventId;
+    }
+
+    public static Builder newBuilder(String eventName) {
+        return new Builder(eventName);
+    }
+
+    public static Builder newBuilder(String eventName, String eventId) {
+        return new Builder(eventName, eventId);
+    }
+
+    public static class Builder {
+        public JSONObject properties = new JSONObject();
+        public String eventName;
+        public String eventId;
+        public Builder(String eventName){
+            this.eventName = eventName;
+        }
+        public Builder(String eventName, String eventId){
+            this.eventName = eventName;
+            this.eventId = eventId;
+        }
+        public Builder addProperty(String key, Object value) {
+            safeAddProperty(key, value);
+            return this;
+        }
+
+        public Builder addProperty(String key, String value) {
+            safeAddProperty(key, value);
+            return this;
+        }
+
+        public Builder addProperty(String key, boolean value) {
+            safeAddProperty(key, value);
+            return this;
+        }
+
+        public Builder addProperty(String key, double value) {
+            safeAddProperty(key, value);
+            return this;
+        }
+
+        public Builder addProperty(String key, int value) {
+            safeAddProperty(key, value);
+            return this;
+        }
+
+        public Builder addProperty(String key, long value) {
+            safeAddProperty(key, value);
+            return this;
+        }
+
+        private void safeAddProperty(String key, Object value) {
+            try {
+                properties.put(key, value);
+            } catch (JSONException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        public TTBaseEvent build() {
+            TTBaseEvent event = new TTBaseEvent(eventName, properties, eventId);
+            return event;
+        }
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTAddToCartEvent.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTAddToCartEvent.java
new file mode 100644
index 0000000..b00c0c7
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTAddToCartEvent.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_ADD_TO_CARD;
+
+import org.json.JSONObject;
+
+public class TTAddToCartEvent extends TTContentsEvent {
+
+    TTAddToCartEvent(String eventName, JSONObject properties, String eventId) {
+        super(eventName, properties, eventId);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder(EVENT_NAME_ADD_TO_CARD, "");
+    }
+
+    public static Builder newBuilder(String eventId) {
+        return new Builder(EVENT_NAME_ADD_TO_CARD, eventId);
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTAddToWishlistEvent.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTAddToWishlistEvent.java
new file mode 100644
index 0000000..7909c25
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTAddToWishlistEvent.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_ADD_TO_WISHLIST;
+
+import org.json.JSONObject;
+
+public class TTAddToWishlistEvent extends TTContentsEvent {
+
+    TTAddToWishlistEvent(String eventName, JSONObject properties, String eventId) {
+        super(eventName, properties, eventId);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder(EVENT_NAME_ADD_TO_WISHLIST, "");
+    }
+
+    public static Builder newBuilder(String eventId) {
+        return new Builder(EVENT_NAME_ADD_TO_WISHLIST, eventId);
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTCheckoutEvent.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTCheckoutEvent.java
new file mode 100644
index 0000000..5352714
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTCheckoutEvent.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_CHECK_OUT;
+
+import org.json.JSONObject;
+
+public class TTCheckoutEvent extends TTContentsEvent {
+
+    TTCheckoutEvent(String eventName, JSONObject properties, String eventId) {
+        super(eventName, properties, eventId);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder(EVENT_NAME_CHECK_OUT, "");
+    }
+
+    public static Builder newBuilder(String eventId) {
+        return new Builder(EVENT_NAME_CHECK_OUT, eventId);
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTContentParams.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTContentParams.java
new file mode 100644
index 0000000..40d491f
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTContentParams.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+
+import android.text.TextUtils;
+
+import org.json.JSONObject;
+
+public class TTContentParams {
+    private float price;
+    private int quantity;
+    private String contentId;
+    private String contentCategory;
+    private String contentName;
+    private String brand;
+    private boolean priceAvailable = false;
+    private boolean quantityAvailable = false;
+
+    public static TTContentParams.Builder newBuilder() {
+        return new TTContentParams.Builder();
+    }
+
+    public static class Builder {
+        private float price = Float.NaN;
+        private int quantity = -1;
+        private String contentId;
+        private String contentCategory;
+        private String contentName;
+        private String brand;
+        private boolean priceAvailable = false;
+        private boolean quantityAvailable = false;
+
+        public Builder setPrice(float price) {
+            this.price = price;
+            priceAvailable = true;
+            return this;
+        }
+
+        public Builder setQuantity(int quantity) {
+            this.quantity = quantity;
+            quantityAvailable = true;
+            return this;
+        }
+
+        public Builder setContentId(String contentId) {
+            this.contentId = contentId;
+            return this;
+        }
+
+        public Builder setContentCategory(String contentCategory) {
+            this.contentCategory = contentCategory;
+            return this;
+        }
+
+        public Builder setContentName(String contentName) {
+            this.contentName = contentName;
+            return this;
+        }
+
+        public Builder setBrand(String brand) {
+            this.brand = brand;
+            return this;
+        }
+
+        public TTContentParams build() {
+            TTContentParams params = new TTContentParams();
+            params.price = price;
+            params.priceAvailable = priceAvailable;
+            params.quantity = quantity;
+            params.quantityAvailable = quantityAvailable;
+            params.contentId = contentId;
+            params.contentCategory = contentCategory;
+            params.contentName = contentName;
+            params.brand = brand;
+            return params;
+        }
+    }
+
+    public JSONObject toJSONObject(){
+        JSONObject jsonObject = null;
+        try{
+            jsonObject = new JSONObject();
+            if(quantityAvailable) {
+                jsonObject.put("quantity", quantity);
+            }
+            if(!TextUtils.isEmpty(contentId)){
+                jsonObject.put("content_id", contentId);
+            }
+            if(!TextUtils.isEmpty(contentCategory)) {
+                jsonObject.put("content_category", contentCategory);
+            }
+            if(!TextUtils.isEmpty(contentName)) {
+                jsonObject.put("content_name", contentName);
+            }
+            if(!TextUtils.isEmpty(brand)) {
+                jsonObject.put("brand", brand);
+            }
+            if (priceAvailable && price != Float.NaN) {
+                jsonObject.put("price", price);
+            }
+        }catch (Throwable e){
+            e.printStackTrace();
+        }
+        return jsonObject;
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTContentsEvent.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTContentsEvent.java
new file mode 100644
index 0000000..59b539e
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTContentsEvent.java
@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENTS;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENT_ID;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENT_TYPE;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CURRENCY;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_DESCRIPTION;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_VALUE;
+
+import android.text.TextUtils;
+
+import com.tiktok.appevents.base.TTBaseEvent;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class TTContentsEvent extends TTBaseEvent {
+    TTContentsEvent(String eventName, JSONObject properties, String eventId) {
+        super(eventName, properties, eventId);
+    }
+
+    public static class Builder extends TTBaseEvent.Builder{
+
+        Builder(String eventName, String eventId) {
+            super(eventName, eventId);
+        }
+
+        public Builder setDescription(String description) {
+            if(!TextUtils.isEmpty(description)){
+                addProperty(EVENT_PROPERTY_DESCRIPTION, description);
+            }
+            return this;
+        }
+
+        public Builder setCurrency(TTContentsEventConstants.Currency currency) {
+            if(currency != null) {
+                addProperty(EVENT_PROPERTY_CURRENCY, currency);
+            }
+            return this;
+        }
+
+        public Builder setValue(double value) {
+            safeAddProperty(EVENT_PROPERTY_VALUE, value);
+            return this;
+        }
+
+        public Builder setContentType(String contentType) {
+            if(!TextUtils.isEmpty(contentType)) {
+                addProperty(EVENT_PROPERTY_CONTENT_TYPE, contentType);
+            }
+            return this;
+        }
+
+        public Builder setContentId(String contentId) {
+            if(!TextUtils.isEmpty(contentId)) {
+                addProperty(EVENT_PROPERTY_CONTENT_ID, contentId);
+            }
+            return this;
+        }
+
+        public Builder setContents(TTContentParams... contents) {
+            if (contents != null) {
+                JSONArray jsonArray = new JSONArray();
+                for (TTContentParams content : contents) {
+                    if(content != null) {
+                        jsonArray.put(content.toJSONObject());
+                    }
+                }
+                addProperty(EVENT_PROPERTY_CONTENTS, jsonArray);
+            }
+            return this;
+        }
+
+        private void safeAddProperty(String key, Object value) {
+            try {
+                properties.put(key, value);
+            } catch (Throwable e) {}
+        }
+
+        public TTContentsEvent build() {
+            TTContentsEvent event = new TTContentsEvent(eventName, properties, eventId);
+            return event;
+        }
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTContentsEventConstants.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTContentsEventConstants.java
new file mode 100644
index 0000000..b59b86d
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTContentsEventConstants.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+public interface TTContentsEventConstants {
+    interface ContentsEventName {
+        String EVENT_NAME_ADD_TO_CARD = "AddToCart";
+        String EVENT_NAME_ADD_TO_WISHLIST = "AddToWishlist";
+        String EVENT_NAME_CHECK_OUT = "Checkout";
+        String EVENT_NAME_PURCHASE = "Purchase";
+        String EVENT_NAME_VIEW_CONTENT = "ViewContent";
+
+    }
+
+    interface Params {
+        String EVENT_PROPERTY_CONTENT_TYPE = "content_type";
+        String EVENT_PROPERTY_CONTENT_ID = "content_id";
+        String EVENT_PROPERTY_DESCRIPTION = "description";
+        String EVENT_PROPERTY_CURRENCY = "currency";
+        String EVENT_PROPERTY_VALUE = "value";
+        String EVENT_PROPERTY_CONTENTS = "contents";
+    }
+
+    enum Currency {
+        AED, ARS, AUD, BDT, BHD, BIF, BOB, BRL, CAD, CHF, CLP, CNY, COP, CRC, CZK, DKK, DZD, EGP,
+        EUR, GBP, GTQ, HKD, HNL, HUF, IDR, ILS, INR, ISK, JPY, KES, KHR, KRW, KWD, KZT, MAD, MOP,
+        MXN, MYR, NGN, NIO, NOK, NZD, OMR, PEN, PHP, PKR, PLN, PYG, QAR, RON, RUB, SAR, SEK, SGD,
+        THB, TRY, TWD, UAH, USD, VES, VND, ZAR
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTPurchaseEvent.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTPurchaseEvent.java
new file mode 100644
index 0000000..b7e8414
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTPurchaseEvent.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_PURCHASE;
+
+import org.json.JSONObject;
+
+public class TTPurchaseEvent extends TTContentsEvent {
+
+    TTPurchaseEvent(String eventName, JSONObject properties, String eventId) {
+        super(eventName, properties, eventId);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder(EVENT_NAME_PURCHASE, "");
+    }
+
+    public static Builder newBuilder(String eventId) {
+        return new Builder(EVENT_NAME_PURCHASE, eventId);
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/appevents/contents/TTViewContentEvent.java b/business-core/src/main/java/com/tiktok/appevents/contents/TTViewContentEvent.java
new file mode 100644
index 0000000..f1911aa
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/appevents/contents/TTViewContentEvent.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.appevents.contents;
+
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_VIEW_CONTENT;
+
+import org.json.JSONObject;
+
+public class TTViewContentEvent extends TTContentsEvent {
+
+    TTViewContentEvent(String eventName, JSONObject properties, String eventId) {
+        super(eventName, properties, eventId);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder(EVENT_NAME_VIEW_CONTENT, "");
+    }
+
+    public static Builder newBuilder(String eventId) {
+        return new Builder(EVENT_NAME_VIEW_CONTENT, eventId);
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/iap/TTInAppPurchaseWrapper.java b/business-core/src/main/java/com/tiktok/iap/TTInAppPurchaseWrapper.java
new file mode 100644
index 0000000..975393b
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/iap/TTInAppPurchaseWrapper.java
@@ -0,0 +1,164 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.tiktok.iap;
+
+import static com.tiktok.TikTokBusinessSdk.getApplicationContext;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.android.billingclient.api.BillingClient;
+import com.android.billingclient.api.BillingClientStateListener;
+import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.Purchase;
+import com.android.billingclient.api.PurchasesUpdatedListener;
+import com.android.billingclient.api.SkuDetails;
+import com.android.billingclient.api.SkuDetailsParams;
+import com.tiktok.TikTokBusinessSdk;
+import com.tiktok.appevents.TTPurchaseInfo;
+import com.tiktok.util.TTLogger;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TTInAppPurchaseWrapper {
+
+    private static Context mContext;
+    private static BillingClient mBillingClient;
+    static final String TAG = TTInAppPurchaseWrapper.class.getName();
+
+    private static final TTLogger ttLogger = new TTLogger(TAG, TikTokBusinessSdk.getLogLevel());
+
+    public static void registerIapTrack() {
+        try {
+            if (getApplicationContext() == null) {
+                return;
+            }
+            mContext = getApplicationContext();
+            PurchasesUpdatedListener purchaseUpdateListener = (billingResult, purchases) -> {
+
+                if (billingResult != null && billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
+                        && purchases != null) {
+                    for (Purchase purchase : purchases) {
+                        if (purchase == null) {
+                            continue;
+                        }
+                        List skus = purchase.getSkus();
+                        if (skus == null || skus.size() == 0) {
+                            continue;
+                        }
+                        querySkuAndTrack(skus, purchase, true);
+                    }
+                } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
+                    ttLogger.info("user canceled");
+                } else {
+                    ttLogger.info("otherErr : %s", billingResult.getDebugMessage());
+                }
+            };
+            mBillingClient = BillingClient.newBuilder(mContext)
+                    .setListener(purchaseUpdateListener)
+                    .enablePendingPurchases()
+                    .build();
+            startBillingClient();
+        } catch (Throwable ignored) {
+            ttLogger.error(ignored, "register Iap track error");
+        }
+    }
+
+    public static void startBillingClient() {
+        try {
+            if (mBillingClient == null || mBillingClient.isReady()) {
+                return;
+            }
+            mBillingClient.startConnection(new BillingClientStateListener() {
+                @Override
+                public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
+                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+                        ttLogger.info("billing setup finished");
+                    } else {
+                        ttLogger.info("billing setup error %s", billingResult.getDebugMessage());
+                    }
+                }
+
+                @Override
+                public void onBillingServiceDisconnected() {
+                    ttLogger.info("billing service disconnected");
+                }
+            });
+        } catch (Throwable ignored) {
+            ttLogger.error(ignored, "start billing client connection error");
+        }
+    }
+
+    private static void querySkuAndTrack(List skus, Purchase purchase, boolean isInAppPurchase) {
+        try {
+            List skuList = new ArrayList<>();
+            for (String sku : skus) {
+                if (sku == null || sku.isEmpty()) {
+                    continue;
+                }
+                skuList.add(sku);
+            }
+            SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
+            if (isInAppPurchase) {
+                params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
+            } else {
+                params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
+            }
+            mBillingClient.querySkuDetailsAsync(params.build(), (billingResult, skuDetailsList) -> {
+                if (billingResult != null && billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
+                        && skuDetailsList != null) {
+                    if (skuDetailsList.size() > 0) {
+                        List purchaseInfos = new ArrayList<>();
+                        try {
+                            for (SkuDetails skuDetails : skuDetailsList) {
+                                purchaseInfos.add(new TTPurchaseInfo(new JSONObject(purchase.getOriginalJson()),
+                                        new JSONObject(skuDetails.getOriginalJson())));
+                            }
+                            TikTokBusinessSdk.trackGooglePlayPurchase(purchaseInfos);
+                        } catch (Throwable e) {
+                            ttLogger.error(e, "query Sku And Track google play purchase error");
+                        }
+                    } else {
+                        if (isInAppPurchase) {
+                            querySkuAndTrack(skus, purchase, false);
+                        } else {
+                            sendNoSkuIapTrack(skus, purchase);
+                        }
+                    }
+                } else {
+                    sendNoSkuIapTrack(skus, purchase);
+                }
+            });
+        } catch (Throwable ignored) {
+            ttLogger.error(ignored, "query Sku And Track error");
+        }
+    }
+
+    private static void sendNoSkuIapTrack(List skus, Purchase purchase) {
+        try {
+            JSONArray contents = new JSONArray();
+            for (String sku : skus) {
+                if (sku == null || sku.isEmpty()) {
+                    continue;
+                }
+                JSONObject item = new JSONObject()
+                        .put("quantity", purchase.getQuantity())
+                        .put("content_id", sku);
+                contents.put(item);
+            }
+            JSONObject content = new JSONObject().put("contents", contents);
+            TikTokBusinessSdk.trackEvent("Purchase", content);
+        } catch (Throwable ignored) {
+            ttLogger.error(ignored, "Track Purchase error");
+        }
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/util/HttpRequestUtil.java b/business-core/src/main/java/com/tiktok/util/HttpRequestUtil.java
index 709c213..ed4c594 100644
--- a/business-core/src/main/java/com/tiktok/util/HttpRequestUtil.java
+++ b/business-core/src/main/java/com/tiktok/util/HttpRequestUtil.java
@@ -6,6 +6,8 @@
 
 package com.tiktok.util;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_NET_ERROR;
+
 import androidx.annotation.Nullable;
 import com.tiktok.TikTokBusinessSdk;
 import com.tiktok.appevents.TTCrashHandler;
@@ -45,6 +47,8 @@ public void configConnection(HttpURLConnection connection) {
 
     private static final String TAG = HttpRequestUtil.class.getCanonicalName();
 
+    private static final TTLogger ttLogger = new TTLogger(TAG, TikTokBusinessSdk.getLogLevel());
+
     public static String doGet(String url, Map headerParamMap) {
         HttpRequestOptions options = new HttpRequestOptions();
         options.connectTimeout = 2000;
@@ -75,12 +79,12 @@ public static HttpsURLConnection connect(String url, Map headerP
 
             connection.connect();
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_NET_ERROR);
             if (connection != null) {
                 try {
                     connection.disconnect();
                 }catch (Exception exc){
-                    TTCrashHandler.handleCrash(TAG, exc);
+                    TTCrashHandler.handleCrash(TAG, exc, TTSDK_EXCEPTION_NET_ERROR);
                 }
             }
         }
@@ -126,13 +130,13 @@ public static String doGet(String url, Map headerParamMap, HttpR
                 result = streamToString(connection.getInputStream());
             }
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_NET_ERROR);
         } finally {
             if (connection != null) {
                 try {
                     connection.disconnect();
                 } catch (Exception e) {
-                    TTCrashHandler.handleCrash(TAG, e);
+                    TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_NET_ERROR);
                 }
             }
         }
@@ -198,21 +202,25 @@ public static String doPost(String url, Map headerParamMap, Stri
             if (responseCode == HttpURLConnection.HTTP_OK) {
                 result = streamToString(connection.getInputStream());
             }
+            if(TikTokBusinessSdk.isInSdkDebugMode()) {
+                ttLogger.info("doPost request body: %s", jsonStr);
+                ttLogger.info("doPost result: %s", result == null ? String.valueOf(responseCode) : result);
+            }
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_NET_ERROR);
         } finally {
             if (outputStream != null) {
                 try {
                     outputStream.close();
                 } catch (IOException e) {
-                    TTCrashHandler.handleCrash(TAG, e);
+                    TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_NET_ERROR);
                 }
             }
             if (connection != null) {
                 try {
                     connection.disconnect();
                 } catch (Exception e){
-                    TTCrashHandler.handleCrash(TAG, e);
+                    TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_NET_ERROR);
                 }
             }
         }
@@ -239,7 +247,7 @@ private static String streamToString(InputStream is) {
             }
             return sb.toString().trim();
         } catch (Exception e) {
-            TTCrashHandler.handleCrash(TAG, e);
+            TTCrashHandler.handleCrash(TAG, e, TTSDK_EXCEPTION_NET_ERROR);
         }
         return null;
     }
diff --git a/business-core/src/main/java/com/tiktok/util/RegexUtil.java b/business-core/src/main/java/com/tiktok/util/RegexUtil.java
new file mode 100644
index 0000000..d86fd1a
--- /dev/null
+++ b/business-core/src/main/java/com/tiktok/util/RegexUtil.java
@@ -0,0 +1,20 @@
+package com.tiktok.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RegexUtil {
+    public static boolean validateAppId(String appId) {
+        String appIdRegex = "^[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)+$";
+        Pattern pattern = Pattern.compile(appIdRegex);
+        Matcher matcher = pattern.matcher(appId);
+        return matcher.matches();
+    }
+
+    public static boolean validateTTAppId(String ttAppId) {
+        String ttAppIdRegex = "^(\\d+,)*\\d+$";
+        Pattern pattern = Pattern.compile(ttAppIdRegex);
+        Matcher matcher = pattern.matcher(ttAppId);
+        return matcher.matches();
+    }
+}
diff --git a/business-core/src/main/java/com/tiktok/util/SystemInfoUtil.java b/business-core/src/main/java/com/tiktok/util/SystemInfoUtil.java
index ff50411..dcd4beb 100644
--- a/business-core/src/main/java/com/tiktok/util/SystemInfoUtil.java
+++ b/business-core/src/main/java/com/tiktok/util/SystemInfoUtil.java
@@ -6,6 +6,8 @@
 
 package com.tiktok.util;
 
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
+
 import android.app.Application;
 import android.content.Context;
 import android.content.pm.PackageInfo;
@@ -112,7 +114,7 @@ public static void initUserAgent() {
         if (userAgent == null) userAgent = "";
         long endTimeMS = System.currentTimeMillis();
         try {
-            JSONObject meta = TTUtil.getMetaException(ex, endTimeMS)
+            JSONObject meta = TTUtil.getMetaException(ex, endTimeMS, TTSDK_EXCEPTION_SDK_CATCH)
                     .put("latency", endTimeMS-initTimeMS);
             TikTokBusinessSdk.getAppEventLogger().monitorMetric("ua_end", meta, null);
         } catch (Exception ignored) {}
diff --git a/business-core/src/main/java/com/tiktok/util/TTConst.java b/business-core/src/main/java/com/tiktok/util/TTConst.java
index c1ca429..d634ee5 100644
--- a/business-core/src/main/java/com/tiktok/util/TTConst.java
+++ b/business-core/src/main/java/com/tiktok/util/TTConst.java
@@ -15,6 +15,9 @@ public class TTConst {
     public static final String TTSDK_APP_2DR_TIME = "com.tiktok.sdk.2drTime";
 
     public static final String TTSDK_PREFIX = "com.tiktok";
+    public static final int TTSDK_EXCEPTION_NET_ERROR = 1;
+    public static final int TTSDK_EXCEPTION_SDK_CATCH = 2;
+    public static final int TTSDK_EXCEPTION_CRASH = 3;
 
     public static enum ApiErrorCodes {
         PARTIAL_SUCCESS(20001),
diff --git a/business-core/src/main/java/com/tiktok/util/TTUtil.java b/business-core/src/main/java/com/tiktok/util/TTUtil.java
index 768072f..dafc2b0 100644
--- a/business-core/src/main/java/com/tiktok/util/TTUtil.java
+++ b/business-core/src/main/java/com/tiktok/util/TTUtil.java
@@ -7,6 +7,7 @@
 package com.tiktok.util;
 
 import android.content.Context;
+import android.net.Uri;
 import android.os.Looper;
 
 import androidx.annotation.NonNull;
@@ -22,6 +23,7 @@
 import java.util.UUID;
 
 import static com.tiktok.util.TTConst.TTSDK_APP_ANONYMOUS_ID;
+import static com.tiktok.util.TTConst.TTSDK_EXCEPTION_SDK_CATCH;
 
 public class TTUtil {
     private static final String TAG = TTUtil.class.getName();
@@ -35,7 +37,7 @@ public class TTUtil {
      */
     public static void checkThread(String tag) {
         if (Looper.getMainLooper() == Looper.myLooper()) {
-            TTCrashHandler.handleCrash(tag, new IllegalStateException("Current method should be called in a non-main thread"));
+            TTCrashHandler.handleCrash(tag, new IllegalStateException("Current method should be called in a non-main thread"), TTSDK_EXCEPTION_SDK_CATCH);
         }
     }
 
@@ -76,31 +78,15 @@ public static String getOrGenAnoId(Context context, boolean forceGenerate) {
         return anoId;
     }
 
-    public static String escapeHTML(String s) {
-        StringBuilder out = new StringBuilder(Math.max(16, s.length()));
-        for (int i = 0; i < s.length(); i++) {
-            char c = s.charAt(i);
-            if (c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') {
-                out.append("&#");
-                out.append((int) c);
-                out.append(';');
-            } else {
-                out.append(c);
-            }
-        }
-        return out.toString();
-    }
-
-    public static String mapToString(Map map, String separator) {
+    public static String mapToString(String url, Map map) {
         if (map.isEmpty()) {
-            return "";
+            return url;
         }
-        StringBuffer buf = new StringBuffer();
+        Uri.Builder build = Uri.parse(url).buildUpon();
         for (Map.Entry entry : map.entrySet()) {
-            buf.append(entry.getKey() + "=" + escapeHTML(entry.getValue().toString()) + "&");
+            build.appendQueryParameter(entry.getKey(), entry.getValue().toString());
         }
-        String finalStr = buf.toString();
-        return finalStr.substring(0, finalStr.length() - 1);
+        return build.toString();
     }
 
     public static JSONObject getMetaWithTS(@Nullable Long ts) {
@@ -113,18 +99,18 @@ public static JSONObject getMetaWithTS(@Nullable Long ts) {
         return new JSONObject();
     }
 
-    public static JSONObject getMonitorException(@Nullable Throwable ex, @Nullable Long ts) {
+    public static JSONObject getMonitorException(@Nullable Throwable ex, @Nullable Long ts, int type) {
         JSONObject monitor = new JSONObject();
         try {
             monitor.put("type", "exception");
             monitor.put("name", "exception");
-            monitor.put("meta", getMetaException(ex, ts));
+            monitor.put("meta", getMetaException(ex, ts, type));
             monitor.put("extra", null);
         } catch (Exception ignored) {}
         return monitor;
     }
 
-    public static JSONObject getMetaException(@Nullable Throwable ex, @Nullable Long ts) {
+    public static JSONObject getMetaException(@Nullable Throwable ex, @Nullable Long ts, int type) {
         JSONObject meta = getMetaWithTS(ts);
         try {
             if (ex != null) {
@@ -137,6 +123,7 @@ public static JSONObject getMetaException(@Nullable Throwable ex, @Nullable Long
                         " " + rootCause.getStackTrace()[0].getLineNumber();
                 meta.put("ex_args", argMsg);
                 meta.put("ex_msg", rootCause.getMessage());
+                meta.put("ex_type", type);
                 final int stackLimit = 15;
                 String[] st = new String[stackLimit];
                 for(int i = 0; i < stackLimit; i++) {
diff --git a/business-core/src/test/java/com/tiktok/appevents/TTAppEventLoggerTest.java b/business-core/src/test/java/com/tiktok/appevents/TTAppEventLoggerTest.java
index 0fa5ae4..ba8b9bb 100644
--- a/business-core/src/test/java/com/tiktok/appevents/TTAppEventLoggerTest.java
+++ b/business-core/src/test/java/com/tiktok/appevents/TTAppEventLoggerTest.java
@@ -108,9 +108,9 @@ public void globalSwitchOnButNetworkOnButAccessTokenNull() {
         appEventLogger.flush(TTAppEventLogger.FlushReason.FORCE_FLUSH);
     }
 
-    TTAppEvent fromDisk1 = new TTAppEvent(TTAppEvent.TTAppEventType.track, "InternalTest", "{}");
-    TTAppEvent fromDisk2 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}");
-    TTAppEvent fromMemory3 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}");
+    TTAppEvent fromDisk1 = new TTAppEvent(TTAppEvent.TTAppEventType.track, "InternalTest", "{}", null, null);
+    TTAppEvent fromDisk2 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null);
+    TTAppEvent fromMemory3 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null);
 
     private TTAppEventLogger flushCommon() {
         PowerMockito.mockStatic(TikTokBusinessSdk.class);
@@ -164,7 +164,7 @@ public void flushNormally() {
     public void flushFailed() {
         TTAppEventLogger appEventLogger = flushCommon();
         List failed = new LinkedList<>();
-        failed.add(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}"));
+        failed.add(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null));
 
         // when failed to flush, persist to disk
         when(TTRequest.reportAppEvent(any(), any())).thenReturn(failed);
diff --git a/business-core/src/test/java/com/tiktok/appevents/TTAppEventsQueueTest.java b/business-core/src/test/java/com/tiktok/appevents/TTAppEventsQueueTest.java
index 16de4f4..e0d542b 100644
--- a/business-core/src/test/java/com/tiktok/appevents/TTAppEventsQueueTest.java
+++ b/business-core/src/test/java/com/tiktok/appevents/TTAppEventsQueueTest.java
@@ -28,8 +28,8 @@ public class TTAppEventsQueueTest extends BaseTest {
 
     @Test
     public void simpleCase() {
-        TTAppEventsQueue.addEvent(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}"));
-        TTAppEventsQueue.addEvent(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}"));
+        TTAppEventsQueue.addEvent(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null));
+        TTAppEventsQueue.addEvent(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null));
 
         assertEquals(2, TTAppEventsQueue.size());
         TTAppEventsQueue.clearAll();
@@ -58,15 +58,15 @@ public void thresholdLeft(int threshold, int left) {
 
             }
         });
-        TTAppEventsQueue.addEvent(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}"));
+        TTAppEventsQueue.addEvent(new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null));
         TikTokBusinessSdk.destroy();
         TTAppEventsQueue.clearAll();
     }
 
     @Test
     public void testExport() {
-        TTAppEvent e1 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}");
-        TTAppEvent e2 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}");
+        TTAppEvent e1 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null);
+        TTAppEvent e2 = new TTAppEvent(TTAppEvent.TTAppEventType.track,"InternalTest", "{}", null, null);
         TTAppEventsQueue.addEvent(e1);
         TTAppEventsQueue.addEvent(e2);
 
diff --git a/gradle.properties b/gradle.properties
index 51862c9..1719fdb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,7 +11,7 @@ android.enableJetifier=true
 
 GROUP=com.tiktok
 
-VERSION_NAME=1.2.11
+VERSION_NAME=1.3.1
 
 mavenGroupId = com.tiktok
 mavenArtifactId = tiktok-business-android-sdk
diff --git a/jitpack.yml b/jitpack.yml
new file mode 100644
index 0000000..46c8529
--- /dev/null
+++ b/jitpack.yml
@@ -0,0 +1,2 @@
+jdk:
+  - openjdk11
\ No newline at end of file
diff --git a/samples/TestApp/build.gradle b/samples/TestApp/build.gradle
index 8478846..9ccbbaf 100644
--- a/samples/TestApp/build.gradle
+++ b/samples/TestApp/build.gradle
@@ -7,21 +7,14 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdkVersion 33
     buildToolsVersion "30.0.2"
-
     defaultConfig {
-        applicationId "com.example"
+        applicationId "com.tiktok.iabtest"
         minSdkVersion 17
-        targetSdkVersion 31
-        versionCode 1
-        versionName "0.0.1-alpha"
-
-        javaCompileOptions {
-            annotationProcessorOptions {
-                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
-            }
-        }
+        targetSdkVersion 33
+        versionCode 14
+        versionName "1.14"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
@@ -32,11 +25,6 @@ android {
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
         }
     }
-
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
 }
 
 dependencies {
@@ -51,7 +39,7 @@ dependencies {
     implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
     implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
 
-    implementation 'com.android.billingclient:billing:3.0.3'
+    implementation 'com.android.billingclient:billing:6.0.0'
 
     implementation 'androidx.room:room-runtime:2.4.3'
     implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
diff --git a/samples/TestApp/src/main/AndroidManifest.xml b/samples/TestApp/src/main/AndroidManifest.xml
index e3720a2..80c9e03 100644
--- a/samples/TestApp/src/main/AndroidManifest.xml
+++ b/samples/TestApp/src/main/AndroidManifest.xml
@@ -11,6 +11,10 @@
     
     
     
+    
+    
+    
+    
 
      android.util.Log.i("TikTokBusinessSdk", "setOnCrashListener" + thread.getName(), ex));
-
-            // testing delay tracking, implementing a 6 sec delay manually
-            // ideally has to be after accepting tracking permission
-             new Handler(Looper.getMainLooper()).postDelayed(TikTokBusinessSdk::startTrack, 10000);
-        }
     }
 
     @Override
@@ -105,7 +78,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
                         for (String prop : Objects.requireNonNull(TestEvents.TTEventProperties.get(event))) {
                             props.put(prop, "");
                         }
-                        eventLogRepo.save(new EventLog(event, props.toString()));
+                        eventLogRepo.save(new EventLog(event, props.toString()), false);
                     } catch (JSONException e) {
                         e.printStackTrace();
                     }
@@ -122,7 +95,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
                 }
                 while (count < MAX && logs.size() > 0) {
                     for (EventLog log : Objects.requireNonNull(logs)) {
-                        eventLogRepo.save(new EventLog(log.eventType, log.properties));
+                        eventLogRepo.save(new EventLog(log.eventType, log.properties), false);
                         count++;
                         if (count >= MAX) break;
                     }
diff --git a/samples/TestApp/src/main/java/com/example/persistence/EventLogRepo.java b/samples/TestApp/src/main/java/com/example/persistence/EventLogRepo.java
index de8c0a9..8a07abc 100644
--- a/samples/TestApp/src/main/java/com/example/persistence/EventLogRepo.java
+++ b/samples/TestApp/src/main/java/com/example/persistence/EventLogRepo.java
@@ -6,14 +6,38 @@
 
 package com.example.persistence;
 
+import static com.example.testdata.TestEvents.TTBaseEvents;
+import static com.example.testdata.TestEvents.TTContentsEvent;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_ADD_TO_CARD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_ADD_TO_WISHLIST;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_CHECK_OUT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_PURCHASE;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_VIEW_CONTENT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENTS;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENT_ID;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENT_TYPE;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CURRENCY;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_DESCRIPTION;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_VALUE;
+
 import android.app.Application;
 import android.os.AsyncTask;
 
 import androidx.lifecycle.LiveData;
 
 import com.example.model.EventLog;
+import com.example.testdata.TestEvents;
 import com.tiktok.TikTokBusinessSdk;
+import com.tiktok.appevents.base.TTBaseEvent;
+import com.tiktok.appevents.contents.TTAddToCartEvent;
+import com.tiktok.appevents.contents.TTAddToWishlistEvent;
+import com.tiktok.appevents.contents.TTCheckoutEvent;
+import com.tiktok.appevents.contents.TTContentParams;
+import com.tiktok.appevents.contents.TTContentsEvent;
+import com.tiktok.appevents.contents.TTPurchaseEvent;
+import com.tiktok.appevents.contents.TTViewContentEvent;
 
+import org.json.JSONArray;
 import org.json.JSONObject;
 
 import java.util.Iterator;
@@ -51,16 +75,77 @@ public List getLogs() throws ExecutionException, InterruptedException
         return new getAllAsyncTask(eventLogDao).execute().get();
     }
 
-    public void save(final EventLog eventLog) {
+    public void save(final EventLog eventLog, boolean oldApi) {
         try {
+            if(oldApi){
+                JSONObject props = new JSONObject(eventLog.properties);
+                Iterator iterator = props.keys();
+                JSONObject obj = new JSONObject();
+                while (iterator.hasNext()) {
+                    String key = (String) iterator.next();
+                    obj.put(key, props.get(key));
+                }
+                TikTokBusinessSdk.trackEvent(eventLog.eventType, obj);
+                PersistenceManager.databaseWriteExecutor.execute(() -> eventLogDao.save(eventLog));
+                return;
+            }
             JSONObject props = new JSONObject(eventLog.properties);
-            Iterator iterator = props.keys();
-            JSONObject obj = new JSONObject();
-            while (iterator.hasNext()) {
-                String key = (String) iterator.next();
-                obj.put(key, props.get(key));
+            if(TTBaseEvents.get(eventLog.eventType) != null){
+                TikTokBusinessSdk.trackTTEvent(TTBaseEvents.get(eventLog.eventType));
+            }else if(TTContentsEvent.contains(eventLog.eventType)){
+                TTContentsEvent.Builder info = null;
+                switch (eventLog.eventType){
+                    case EVENT_NAME_ADD_TO_CARD:
+                        info = TTAddToCartEvent.newBuilder();
+                        break;
+                    case EVENT_NAME_ADD_TO_WISHLIST:
+                        info = TTAddToWishlistEvent.newBuilder();
+                        break;
+                    case EVENT_NAME_CHECK_OUT:
+                        info = TTCheckoutEvent.newBuilder();
+                        break;
+                    case EVENT_NAME_PURCHASE:
+                        info = TTPurchaseEvent.newBuilder();
+                        break;
+                    case EVENT_NAME_VIEW_CONTENT:
+                        info = TTViewContentEvent.newBuilder();
+                        break;
+                }
+                if(info != null){
+                    TTContentParams[] params = null;
+                    if(props.has(EVENT_PROPERTY_CONTENTS)){
+                        JSONArray jsonArray = props.optJSONArray(EVENT_PROPERTY_CONTENTS);
+                        params = new TTContentParams[jsonArray.length()];
+                        for (int i = 0; i < jsonArray.length(); i++) {
+                            JSONObject jsonObject = jsonArray.getJSONObject(i);
+                            params[i] = TTContentParams.newBuilder()
+                                    .setContentId(jsonObject.optString("content_id"))
+                                    .setContentCategory(jsonObject.optString("content_category"))
+                                    .setBrand(jsonObject.optString("brand"))
+                                    .setPrice((float) jsonObject.optDouble("price"))
+                                    .setQuantity(jsonObject.optInt("quantity"))
+                                    .setContentName(jsonObject.optString("content_name")).build();
+                        }
+                    }
+                    info.setDescription(props.optString(EVENT_PROPERTY_DESCRIPTION))
+                            .setCurrency(TestEvents.TTCurrency.get(props.optString(EVENT_PROPERTY_CURRENCY)))
+                            .setValue(props.optDouble(EVENT_PROPERTY_VALUE))
+                            .setContentId(props.optString(EVENT_PROPERTY_CONTENT_ID))
+                            .setContentType(props.optString(EVENT_PROPERTY_CONTENT_TYPE));
+                    if(params != null){
+                        info.setContents(params);
+                    }
+                    TikTokBusinessSdk.trackTTEvent(info.build());
+                }
+            }else {
+                Iterator iterator = props.keys();
+                TTBaseEvent.Builder ttBaseEvent = TTBaseEvent.newBuilder(eventLog.eventType);
+                while (iterator.hasNext()) {
+                    String key = (String) iterator.next();
+                    ttBaseEvent.addProperty(key, props.get(key));
+                }
+                TikTokBusinessSdk.trackTTEvent(ttBaseEvent.build());
             }
-            TikTokBusinessSdk.trackEvent(eventLog.eventType, obj);
             PersistenceManager.databaseWriteExecutor.execute(() -> eventLogDao.save(eventLog));
         } catch (Exception ignored) {}
     }
diff --git a/samples/TestApp/src/main/java/com/example/testdata/TestEvents.java b/samples/TestApp/src/main/java/com/example/testdata/TestEvents.java
index d793855..2e7a68b 100644
--- a/samples/TestApp/src/main/java/com/example/testdata/TestEvents.java
+++ b/samples/TestApp/src/main/java/com/example/testdata/TestEvents.java
@@ -6,8 +6,108 @@
 
 package com.example.testdata;
 
+import static com.tiktok.appevents.base.EventName.ACHIEVE_LEVEL;
+import static com.tiktok.appevents.base.EventName.ADD_PAYMENT_INFO;
+import static com.tiktok.appevents.base.EventName.COMPLETE_TUTORIAL;
+import static com.tiktok.appevents.base.EventName.CREATE_GROUP;
+import static com.tiktok.appevents.base.EventName.CREATE_ROLE;
+import static com.tiktok.appevents.base.EventName.GENERATE_LEAD;
+import static com.tiktok.appevents.base.EventName.INSTALL_APP;
+import static com.tiktok.appevents.base.EventName.IN_APP_AD_CLICK;
+import static com.tiktok.appevents.base.EventName.IN_APP_AD_IMPR;
+import static com.tiktok.appevents.base.EventName.JOIN_GROUP;
+import static com.tiktok.appevents.base.EventName.LAUNCH_APP;
+import static com.tiktok.appevents.base.EventName.LOAN_APPLICATION;
+import static com.tiktok.appevents.base.EventName.LOAN_APPROVAL;
+import static com.tiktok.appevents.base.EventName.LOAN_DISBURSAL;
+import static com.tiktok.appevents.base.EventName.LOGIN;
+import static com.tiktok.appevents.base.EventName.RATE;
+import static com.tiktok.appevents.base.EventName.REGISTRATION;
+import static com.tiktok.appevents.base.EventName.SEARCH;
+import static com.tiktok.appevents.base.EventName.SPEND_CREDITS;
+import static com.tiktok.appevents.base.EventName.START_TRIAL;
+import static com.tiktok.appevents.base.EventName.SUBSCRIBE;
+import static com.tiktok.appevents.base.EventName.UNLOCK_ACHIEVEMENT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_ADD_TO_CARD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_ADD_TO_WISHLIST;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_CHECK_OUT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_PURCHASE;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.ContentsEventName.EVENT_NAME_VIEW_CONTENT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.AED;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.ARS;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.AUD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.BDT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.BHD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.BIF;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.BOB;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.BRL;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.CAD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.CHF;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.CLP;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.CNY;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.COP;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.CRC;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.CZK;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.DKK;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.DZD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.EGP;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.EUR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.GBP;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.GTQ;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.HKD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.HNL;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.HUF;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.IDR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.ILS;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.INR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.ISK;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.JPY;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.KES;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.KHR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.KRW;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.KWD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.KZT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.MAD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.MOP;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.MXN;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.MYR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.NGN;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.NIO;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.NOK;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.NZD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.OMR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.PEN;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.PHP;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.PKR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.PLN;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.PYG;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.QAR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.RON;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.RUB;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.SAR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.SEK;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.SGD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.THB;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.TRY;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.TWD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.UAH;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.USD;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.VES;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.VND;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Currency.ZAR;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENT_ID;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENT_TYPE;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CURRENCY;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_DESCRIPTION;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_VALUE;
+
+import com.tiktok.appevents.base.EventName;
+import com.tiktok.appevents.contents.TTContentsEventConstants;
+
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 
 public class TestEvents {
 
@@ -19,176 +119,151 @@ public static String[] getAllEvents() {
         return events.toArray(new String[events.size()]);
     }
 
-    public static HashMap TTEventProperties = new HashMap<>();
+    public static HashMap TTEventProperties = new LinkedHashMap<>();
+    public static HashMap TTBaseEvents = new HashMap<>();
+    public static HashSet TTContentsEvent = new HashSet<>();
+    public static String[] TTContentParams;
+
+    public static HashMap TTCurrency = new HashMap<>();
 
     static {
-        TTEventProperties.put("LaunchAPP", new String[]{});
-        TTEventProperties.put("InstallApp", new String[]{});
-        TTEventProperties.put("2Dretention", new String[]{});
-        TTEventProperties.put("AddPaymentInfo", new String[]{
-                "success",
-        });
-        TTEventProperties.put("AddToCart", new String[]{
-                "content_type",
-                "sku_id",
-                "description",
-                "currency",
-                "value",
-        });
-        TTEventProperties.put("AddToWishlist", new String[]{
-                "page_type",
-                "content_id",
-                "description",
-                "currency",
-                "value",
-        });
-        TTEventProperties.put("Checkout", new String[]{
-                "description",
-                "sku_id",
-                "num_items",
-                "payment_available",
-                "currency",
-                "value",
-                "game_item_type",
-                "game_item_id",
-                "room_type",
-                "currency",
-                "value",
-                "location",
-                "checkin_date",
-                "checkout_date",
-                "number_of_rooms",
-                "number_of_nights",
-        });
-        TTEventProperties.put("CompleteTutorial", new String[]{});
-        TTEventProperties.put("ViewContent", new String[]{
-                "page_type",
-                "sku_id",
-                "description",
-                "currency",
-                "value",
-                "search_string",
-                "room_type",
-                "location",
-                "checkin_date",
-                "checkout_date",
-                "number_of_rooms",
-                "number_of_nights",
-                "outbound_origination_city",
-                "outbound_destination_city",
-                "return_origination_city",
-                "return_destination_city",
-                "class",
-                "number_of_passenger",
-        });
-        TTEventProperties.put("CreateGroup", new String[]{
-                "group_name",
-                "group_logo",
-                "group_description",
-                "group_type",
-                "group_id",
-        });
-        TTEventProperties.put("CreateRole", new String[]{
-                "role_type",
-        });
-        TTEventProperties.put("GenerateLead", new String[]{});
-        TTEventProperties.put("InAppADClick", new String[]{
-                "ad_type",
-        });
-        TTEventProperties.put("InAppADImpr", new String[]{
-                "ad_type",
-        });
-        TTEventProperties.put("JoinGroup", new String[]{
-                "level_number",
+        TTEventProperties.put(ACHIEVE_LEVEL.toString(), new String[]{});
+        TTEventProperties.put(ADD_PAYMENT_INFO.toString(), new String[]{});
+        TTEventProperties.put(EVENT_NAME_ADD_TO_CARD, new String[]{
+                EVENT_PROPERTY_CONTENT_TYPE,EVENT_PROPERTY_CONTENT_ID,EVENT_PROPERTY_DESCRIPTION,
+                EVENT_PROPERTY_CURRENCY,EVENT_PROPERTY_VALUE
         });
-        TTEventProperties.put("AchieveLevel", new String[]{
-                "level_number",
-                "score",
+        TTEventProperties.put(EVENT_NAME_ADD_TO_WISHLIST, new String[]{
+                EVENT_PROPERTY_CONTENT_TYPE,EVENT_PROPERTY_CONTENT_ID,EVENT_PROPERTY_DESCRIPTION,
+                EVENT_PROPERTY_CURRENCY,EVENT_PROPERTY_VALUE
         });
-        TTEventProperties.put("LoanApplication", new String[]{
-                "loan_type",
-                "application_id",
+        TTEventProperties.put(EVENT_NAME_CHECK_OUT, new String[]{
+                EVENT_PROPERTY_CONTENT_TYPE,EVENT_PROPERTY_CONTENT_ID,EVENT_PROPERTY_DESCRIPTION,
+                EVENT_PROPERTY_CURRENCY,EVENT_PROPERTY_VALUE
         });
-        TTEventProperties.put("LoanApproval", new String[]{
-                "value",
+        TTEventProperties.put(COMPLETE_TUTORIAL.toString(), new String[]{});
+        TTEventProperties.put(CREATE_GROUP.toString(), new String[]{});
+        TTEventProperties.put(CREATE_ROLE.toString(), new String[]{});
+        TTEventProperties.put(GENERATE_LEAD.toString(), new String[]{});
+        TTEventProperties.put(IN_APP_AD_CLICK.toString(), new String[]{});
+        TTEventProperties.put(IN_APP_AD_IMPR.toString(), new String[]{});
+        TTEventProperties.put(INSTALL_APP.toString(), new String[]{});
+        TTEventProperties.put(JOIN_GROUP.toString(), new String[]{});
+        TTEventProperties.put(LAUNCH_APP.toString(), new String[]{});
+        TTEventProperties.put(LOAN_APPLICATION.toString(), new String[]{});
+        TTEventProperties.put(LOAN_APPROVAL.toString(), new String[]{});
+        TTEventProperties.put(LOAN_DISBURSAL.toString(), new String[]{});
+        TTEventProperties.put(LOGIN.toString(), new String[]{});
+        TTEventProperties.put(EVENT_NAME_PURCHASE, new String[]{
+                EVENT_PROPERTY_CONTENT_TYPE,EVENT_PROPERTY_CONTENT_ID,EVENT_PROPERTY_DESCRIPTION,
+                EVENT_PROPERTY_CURRENCY,EVENT_PROPERTY_VALUE
         });
-        TTEventProperties.put("LoanDisbursal", new String[]{
-                "value",
-        });
-        // TODO remove LOGIN event? If not, determine the ordering.
-        TTEventProperties.put("Login", new String[]{});
-        TTEventProperties.put("Purchase", new String[]{
-                "page_type",
-                "sku_id",
-                "description",
-                "num_items",
-                "coupon_used",
-                "currency",
-                "value",
-                "group_type",
-                "game_item_id",
-                "room_type",
-                "location",
-                "checkin_date",
-                "checkout_date",
-                "number_of_rooms",
-                "number_of_nights",
-                "outbound_origination_city",
-                "outbound_destination_city",
-                "return_origination_city",
-                "return_destination_city",
-                "class",
-                "number_of_passenger",
-                "service_type",
-                "service_id",
-        });
-        TTEventProperties.put("Rate", new String[]{
-                "page_type",
-                "sku_id",
-                "content",
-                "rating_value",
-                "max_rating_value",
-                "rate",
-        });
-        TTEventProperties.put("Registration", new String[]{
-                "registration_method",
-        });
-        TTEventProperties.put("Search", new String[]{
-                "search_string",
-                "checkin_date",
-                "checkout_date",
-                "number_of_rooms",
-                "number_of_nights",
-                "origination_city",
-                "destination_city",
-                "departure_date",
-                "return_date",
-                "class",
-                "number_of_passenger",
-        });
-        TTEventProperties.put("SpendCredits", new String[]{
-                "game_item_type",
-                "game_item_id",
-                "level_number",
-        });
-        TTEventProperties.put("StartTrial", new String[]{
-                "order_id",
-                "currency",
-        });
-        TTEventProperties.put("Subscribe", new String[]{
-                "order_id",
-                "currency",
-        });
-        TTEventProperties.put("Share", new String[]{
-                "content_type",
-                "content_id",
-                "shared_destination",
-        });
-        TTEventProperties.put("Contact", new String[]{});
-        TTEventProperties.put("UnlockAchievement", new String[]{
-                "description",
-                "achievement_type",
+        TTEventProperties.put(RATE.toString(), new String[]{});
+        TTEventProperties.put(REGISTRATION.toString(), new String[]{});
+        TTEventProperties.put(SEARCH.toString(), new String[]{});
+        TTEventProperties.put(SPEND_CREDITS.toString(), new String[]{});
+        TTEventProperties.put(START_TRIAL.toString(), new String[]{});
+        TTEventProperties.put(SUBSCRIBE.toString(), new String[]{});
+        TTEventProperties.put(UNLOCK_ACHIEVEMENT.toString(), new String[]{});
+        TTEventProperties.put(EVENT_NAME_VIEW_CONTENT, new String[]{
+                EVENT_PROPERTY_CONTENT_TYPE,EVENT_PROPERTY_CONTENT_ID,EVENT_PROPERTY_DESCRIPTION,
+                EVENT_PROPERTY_CURRENCY,EVENT_PROPERTY_VALUE
         });
+        TTEventProperties.put("Test", new String[]{});
+
+        TTBaseEvents.put(ACHIEVE_LEVEL.toString(), ACHIEVE_LEVEL);
+        TTBaseEvents.put(ADD_PAYMENT_INFO.toString(), ADD_PAYMENT_INFO);
+        TTBaseEvents.put(COMPLETE_TUTORIAL.toString(), COMPLETE_TUTORIAL);
+        TTBaseEvents.put(CREATE_GROUP.toString(), CREATE_GROUP);
+        TTBaseEvents.put(CREATE_ROLE.toString(), CREATE_ROLE);
+        TTBaseEvents.put(GENERATE_LEAD.toString(), GENERATE_LEAD);
+        TTBaseEvents.put(IN_APP_AD_CLICK.toString(), IN_APP_AD_CLICK);
+        TTBaseEvents.put(IN_APP_AD_IMPR.toString(), IN_APP_AD_IMPR);
+        TTBaseEvents.put(INSTALL_APP.toString(), INSTALL_APP);
+        TTBaseEvents.put(JOIN_GROUP.toString(), JOIN_GROUP);
+        TTBaseEvents.put(LAUNCH_APP.toString(), LAUNCH_APP);
+        TTBaseEvents.put(LOAN_APPLICATION.toString(), LOAN_APPLICATION);
+        TTBaseEvents.put(LOAN_APPROVAL.toString(), LOAN_APPROVAL);
+        TTBaseEvents.put(LOAN_DISBURSAL.toString(), LOAN_DISBURSAL);
+        TTBaseEvents.put(LOGIN.toString(), LOGIN);
+        TTBaseEvents.put(RATE.toString(), RATE);
+        TTBaseEvents.put(REGISTRATION.toString(), REGISTRATION);
+        TTBaseEvents.put(SEARCH.toString(), SEARCH);
+        TTBaseEvents.put(SPEND_CREDITS.toString(), SPEND_CREDITS);
+        TTBaseEvents.put(START_TRIAL.toString(), START_TRIAL);
+        TTBaseEvents.put(SUBSCRIBE.toString(), SUBSCRIBE);
+        TTBaseEvents.put(UNLOCK_ACHIEVEMENT.toString(), UNLOCK_ACHIEVEMENT);
+
+        TTContentsEvent.add(EVENT_NAME_ADD_TO_CARD);
+        TTContentsEvent.add(EVENT_NAME_ADD_TO_WISHLIST);
+        TTContentsEvent.add(EVENT_NAME_CHECK_OUT);
+        TTContentsEvent.add(EVENT_NAME_PURCHASE);
+        TTContentsEvent.add(EVENT_NAME_VIEW_CONTENT);
+        TTContentParams = new String[]{"price", "quantity", "content_id", "content_category",
+                "content_name", "brand"};
+
+        TTCurrency.put(AED.toString(), AED);
+        TTCurrency.put(ARS.toString(),ARS);
+        TTCurrency.put(AUD.toString(),AUD);
+        TTCurrency.put(BDT.toString(),BDT);
+        TTCurrency.put(BHD.toString(),BHD);
+        TTCurrency.put(BIF.toString(),BIF);
+        TTCurrency.put(BOB.toString(),BOB);
+        TTCurrency.put(BRL.toString(),BRL);
+        TTCurrency.put(CAD.toString(),CAD);
+        TTCurrency.put(CHF.toString(),CHF);
+        TTCurrency.put(CLP.toString(),CLP);
+        TTCurrency.put(CNY.toString(),CNY);
+        TTCurrency.put(EGP.toString(),EGP);
+        TTCurrency.put(IDR.toString(),IDR);
+        TTCurrency.put(ILS.toString(),ILS);
+        TTCurrency.put(OMR.toString(),OMR);
+        TTCurrency.put(VND.toString(),VND);
+        TTCurrency.put(INR.toString(),INR);
+        TTCurrency.put(PEN.toString(),PEN);
+        TTCurrency.put(ZAR.toString(),ZAR);
+        TTCurrency.put(ISK.toString(),ISK);
+        TTCurrency.put(JPY.toString(),JPY);
+        TTCurrency.put(KES.toString(),KES);
+        TTCurrency.put(KHR.toString(),KHR);
+        TTCurrency.put(KRW.toString(),KRW);
+        TTCurrency.put(KWD.toString(),KWD);
+        TTCurrency.put(KZT.toString(),KZT);
+        TTCurrency.put(MAD.toString(),MAD);
+        TTCurrency.put(MOP.toString(),MOP);
+        TTCurrency.put(EUR.toString(),EUR);
+        TTCurrency.put(PHP.toString(),PHP);
+        TTCurrency.put(PKR.toString(),PKR);
+        TTCurrency.put(PLN.toString(),PLN);
+        TTCurrency.put(QAR.toString(),QAR);
+        TTCurrency.put(PYG.toString(),PYG);
+        TTCurrency.put(RON.toString(),RON);
+        TTCurrency.put(MXN.toString(),MXN);
+        TTCurrency.put(RUB.toString(),RUB);
+        TTCurrency.put(SAR.toString(),SAR);
+        TTCurrency.put(SEK.toString(),SEK);
+        TTCurrency.put(SGD.toString(),SGD);
+        TTCurrency.put(THB.toString(),THB);
+        TTCurrency.put(COP.toString(),COP);
+        TTCurrency.put(GBP.toString(),GBP);
+        TTCurrency.put(MYR.toString(),MYR);
+        TTCurrency.put(TRY.toString(),TRY);
+        TTCurrency.put(CRC.toString(),CRC);
+        TTCurrency.put(GTQ.toString(),GTQ);
+        TTCurrency.put(NGN.toString(),NGN);
+        TTCurrency.put(TWD.toString(),TWD);
+        TTCurrency.put(CZK.toString(),CZK);
+        TTCurrency.put(HKD.toString(),HKD);
+        TTCurrency.put(NIO.toString(),NIO);
+        TTCurrency.put(UAH.toString(),UAH);
+        TTCurrency.put(DKK.toString(),DKK);
+        TTCurrency.put(HNL.toString(),HNL);
+        TTCurrency.put(NOK.toString(),NOK);
+        TTCurrency.put(USD.toString(),USD);
+        TTCurrency.put(DZD.toString(),DZD);
+        TTCurrency.put(HUF.toString(),HUF);
+        TTCurrency.put(NZD.toString(),NZD);
+        TTCurrency.put(VES.toString(),VES);
     }
 }
 
diff --git a/samples/TestApp/src/main/java/com/example/ui/eventlog/EventLogViewModel.java b/samples/TestApp/src/main/java/com/example/ui/eventlog/EventLogViewModel.java
index 8b37e92..731669f 100644
--- a/samples/TestApp/src/main/java/com/example/ui/eventlog/EventLogViewModel.java
+++ b/samples/TestApp/src/main/java/com/example/ui/eventlog/EventLogViewModel.java
@@ -27,7 +27,7 @@ public EventLogViewModel(Application application) {
 
     public LiveData> getAllEventLogs() { return eventLogRepo.getAllEventLogs(); }
 
-    public void save(EventLog eventLog) {
-        eventLogRepo.save(eventLog);
+    public void save(EventLog eventLog, boolean oldApi) {
+        eventLogRepo.save(eventLog, oldApi);
     }
 }
\ No newline at end of file
diff --git a/samples/TestApp/src/main/java/com/example/ui/events/EventFragment.java b/samples/TestApp/src/main/java/com/example/ui/events/EventFragment.java
index dcb15cb..a106f41 100644
--- a/samples/TestApp/src/main/java/com/example/ui/events/EventFragment.java
+++ b/samples/TestApp/src/main/java/com/example/ui/events/EventFragment.java
@@ -6,13 +6,20 @@
 
 package com.example.ui.events;
 
+import static com.example.testdata.TestEvents.TTBaseEvents;
+import static com.example.ui.events.PropEditActivity.SOURCE;
+import static com.example.ui.events.PropEditActivity.SOURCE_CONTENT;
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENTS;
+
 import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
+import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
+import android.widget.EditText;
 import android.widget.ImageButton;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -35,6 +42,8 @@
 
 import java.util.Iterator;
 import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
 
 public class EventFragment extends Fragment {
 
@@ -70,6 +79,36 @@ public View onCreateView(@NonNull LayoutInflater inflater,
         View root = inflater.inflate(R.layout.fragment_events, container, false);
 
         final TextView propsTV = root.findViewById(R.id.propsPrettyViewer);
+        final Button propertyBtn = root.findViewById(R.id.addContents);
+        final Button crash = root.findViewById(R.id.crash);
+        final Button trackEvent = root.findViewById(R.id.track_event);
+        final EditText editText = root.findViewById(R.id.number_of_events);
+        final Button sendRandomEvents = root.findViewById(R.id.send_random_event);
+        propertyBtn.setVisibility(View.GONE);
+        sendRandomEvents.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                int number = 100;
+                try{
+                    number = Integer.parseInt(String.valueOf(editText.getText()));
+                }catch (Throwable throwable){
+                    throwable.printStackTrace();
+                }
+                Setset = TTBaseEvents.keySet();
+                String[]key = set.toArray(new String[0]);
+                for(int i =0;i {
             try {
                 assert s != null;
@@ -91,6 +130,9 @@ public View onCreateView(@NonNull LayoutInflater inflater,
             Iterator keys = eventViewModel.getPropIterator();
             while (keys.hasNext()) {
                 String key = keys.next();
+                if(EVENT_PROPERTY_CONTENTS.equals(key)){
+                    continue;
+                }
                 try {
                     bundlePros.putString(key, eventViewModel.getProp(key));
                 } catch (JSONException e) {
@@ -100,6 +142,11 @@ public View onCreateView(@NonNull LayoutInflater inflater,
             intent.putExtras(bundlePros);
             startActivityForResult(intent, 2);
         });
+        propertyBtn.setOnClickListener(view -> {
+            Intent intent = new Intent(requireContext(), PropEditActivity.class);
+            intent.putExtra(SOURCE, SOURCE_CONTENT);
+            startActivityForResult(intent, 2);
+        });
 
         ImageButton savedEventsBtn = root.findViewById(R.id.savedEventsBtn);
         savedEventsBtn.setOnClickListener(v -> {
@@ -109,11 +156,23 @@ public View onCreateView(@NonNull LayoutInflater inflater,
             builder.setItems(events, (dialog, selected) -> {
                 eventViewModel.resetProps();
                 eventViewModel.setEvent(events[selected]);
-                for (String property : Objects.requireNonNull(TestEvents.TTEventProperties.get(events[selected]))) {
-                    try {
-                        eventViewModel.addProp(property, "");
-                    } catch (JSONException e) {
-                        e.printStackTrace();
+                if(TTBaseEvents.containsKey(events[selected])){
+                    propsTV.setVisibility(View.GONE);
+                    propertyBtn.setVisibility(View.GONE);
+                } else {
+                    for (String property : Objects.requireNonNull(TestEvents.TTEventProperties.get(events[selected]))) {
+                        try {
+                            eventViewModel.addProp(property, "");
+                        } catch (JSONException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                    if(TestEvents.TTContentsEvent.contains(events[selected])){
+                        propsTV.setVisibility(View.VISIBLE);
+                        propertyBtn.setVisibility(View.VISIBLE);
+                    } else {
+                        propsTV.setVisibility(View.VISIBLE);
+                        propertyBtn.setVisibility(View.GONE);
                     }
                 }
             });
@@ -128,11 +187,22 @@ public View onCreateView(@NonNull LayoutInflater inflater,
                 eventLogViewModel.save(new EventLog(
                         eventName,
                         Objects.requireNonNull(eventViewModel.getLiveProperties().getValue()).toString()
-                ));
+                ), false);
                 Toast.makeText(requireContext(), eventName + " event tracked, plz check log", Toast.LENGTH_SHORT).show();
             }
         });
 
+        trackEvent.setOnClickListener(view -> {
+            String eventName = eventTV.getText().toString();
+            eventViewModel.setEvent(eventName);
+            if (!eventName.equals("")) {
+                eventLogViewModel.save(new EventLog(
+                        eventName,
+                        Objects.requireNonNull(eventViewModel.getLiveProperties().getValue()).toString()
+                ), true);
+                Toast.makeText(requireContext(), eventName + " event tracked, plz check log", Toast.LENGTH_SHORT).show();
+            }
+        });
         Button flushBtn = root.findViewById(R.id.flush);
         flushBtn.setOnClickListener(v -> {
             TikTokBusinessSdk.flush();
@@ -144,20 +214,44 @@ public View onCreateView(@NonNull LayoutInflater inflater,
     @Override
     public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
+        if(data == null || data.getExtras() == null){
+            return;
+        }
+        Bundle bundle = data.getExtras();
         if (resultCode == 2) {
-            assert data != null;
-            Bundle bundle = data.getExtras();
-            eventViewModel.resetProps();
-            if (bundle != null) {
-                for (String key : bundle.keySet()) {
-                    if (bundle.get(key) != null) {
-                        try {
-                            eventViewModel.addProp(key, bundle.get(key));
-                        } catch (JSONException e) {
-                            e.printStackTrace();
+            try {
+                if (!TextUtils.isEmpty(bundle.getString("event_prop"))) {
+                    eventViewModel.setProps(new JSONObject(bundle.getString("event_prop")));
+                } else {
+                    eventViewModel.resetProps();
+                    for (String key : bundle.keySet()) {
+                        if (bundle.get(key) != null) {
+                            try {
+                                eventViewModel.addProp(key, bundle.get(key));
+                            } catch (JSONException e) {
+                                e.printStackTrace();
+                            }
                         }
                     }
                 }
+            }catch (Throwable throwable){
+                throwable.printStackTrace();
+            }
+        } else if (resultCode == 3) {
+            JSONObject jsonObject = new JSONObject();
+            for (String key : bundle.keySet()) {
+                if (bundle.get(key) != null) {
+                    try {
+                        jsonObject.put(key, bundle.get(key));
+                    } catch (JSONException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+            try {
+                eventViewModel.addContents(jsonObject);
+            } catch (JSONException e) {
+                throw new RuntimeException(e);
             }
         }
     }
diff --git a/samples/TestApp/src/main/java/com/example/ui/events/EventViewModel.java b/samples/TestApp/src/main/java/com/example/ui/events/EventViewModel.java
index b1d314f..78437f0 100644
--- a/samples/TestApp/src/main/java/com/example/ui/events/EventViewModel.java
+++ b/samples/TestApp/src/main/java/com/example/ui/events/EventViewModel.java
@@ -6,6 +6,8 @@
 
 package com.example.ui.events;
 
+import static com.tiktok.appevents.contents.TTContentsEventConstants.Params.EVENT_PROPERTY_CONTENTS;
+
 import android.app.Application;
 import android.os.Build;
 
@@ -14,6 +16,7 @@
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -44,6 +47,10 @@ void resetProps() {
         properties.setValue(new JSONObject());
     }
 
+    void setProps(JSONObject js) {
+        properties.setValue(js);
+    }
+
     void addProp(String key, Object value) throws JSONException {
         JSONObject newProp = properties.getValue();
         assert newProp != null;
@@ -51,9 +58,21 @@ void addProp(String key, Object value) throws JSONException {
         properties.setValue(newProp);
     }
 
+    void addContents(Object value) throws JSONException {
+        JSONObject newProp = properties.getValue();
+        if(newProp.optJSONArray(EVENT_PROPERTY_CONTENTS) != null){
+            newProp.getJSONArray(EVENT_PROPERTY_CONTENTS).put(value);
+        } else {
+            JSONArray jsonArray = new JSONArray();
+            jsonArray.put(value);
+            newProp.put(EVENT_PROPERTY_CONTENTS, jsonArray);
+        }
+        properties.setValue(newProp);
+    }
+
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     String getProp(String key) throws JSONException {
-        return (String) Objects.requireNonNull(properties.getValue()).get(key);
+        return String.valueOf(Objects.requireNonNull(properties.getValue()).get(key));
     }
 
     LiveData getLiveProperties() {
diff --git a/samples/TestApp/src/main/java/com/example/ui/events/PropEditActivity.java b/samples/TestApp/src/main/java/com/example/ui/events/PropEditActivity.java
index 7d2eaa1..9cb7778 100644
--- a/samples/TestApp/src/main/java/com/example/ui/events/PropEditActivity.java
+++ b/samples/TestApp/src/main/java/com/example/ui/events/PropEditActivity.java
@@ -6,6 +6,8 @@
 
 package com.example.ui.events;
 
+import static com.example.testdata.TestEvents.TTContentParams;
+
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AppCompatActivity;
@@ -15,6 +17,7 @@
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
+import android.text.TextUtils;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
@@ -35,9 +38,13 @@
 public class PropEditActivity extends AppCompatActivity {
 
     private ListView myList;
+    private EditText eventProp;
+    public static final String SOURCE_CONTENT = "content";
+    public static final String SOURCE = "source";
     private ArrayList props;
     private PropListAdapter adapter;
     private Integer nextPropID = 0;
+    private int resultCode = 2;
 
     @SuppressLint("CutPasteId")
     @Override
@@ -52,14 +59,24 @@ protected void onCreate(Bundle savedInstanceState) {
 
         myList = findViewById(R.id.list);
         ImageButton fabImageButton = findViewById(R.id.fab_image_button);
+        eventProp = findViewById(R.id.event_prop);
 
         props = new ArrayList<>();
-        Bundle bundle = getIntent().getExtras();
-        if (bundle != null) {
-            for (String key : bundle.keySet()) {
-                if (bundle.get(key) != null) {
-                    nextPropID++;
-                    props.add(new Property(nextPropID.toString(), key, (String) bundle.get(key)));
+        if(SOURCE_CONTENT.equals(getIntent().getStringExtra(SOURCE))){
+            for (String key : TTContentParams) {
+                nextPropID++;
+                props.add(new Property(nextPropID.toString(), key, ""));
+            }
+            resultCode = 3;
+            fabImageButton.setVisibility(View.GONE);
+        }else {
+            Bundle bundle = getIntent().getExtras();
+            if (bundle != null) {
+                for (String key : bundle.keySet()) {
+                    if (bundle.get(key) != null) {
+                        nextPropID++;
+                        props.add(new Property(nextPropID.toString(), key, (String) bundle.get(key)));
+                    }
                 }
             }
         }
@@ -131,15 +148,37 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
             Intent intent= new Intent();
             Bundle bundlePros = new Bundle();
             for (Property p : props) {
-                bundlePros.putString(p.key, p.value);
+                if(!TextUtils.isEmpty(p.value)){
+                    convertValue(bundlePros, p.key, p.value);
+                }
+            }
+            if (!TextUtils.isEmpty(eventProp.getText())) {
+                bundlePros.putString("event_prop", String.valueOf(eventProp.getText()));
+                resultCode = 2;
             }
             intent.putExtras(bundlePros);
-            setResult(2, intent);
+            setResult(resultCode, intent);
             finish();
         }
         return super.onOptionsItemSelected(item);
     }
 
+    private void convertValue(Bundle bundlePros, String key, String value) {
+        switch (key){
+            case "value":
+                bundlePros.putFloat(key, Float.parseFloat(value));
+                break;
+            case "price":
+                bundlePros.putFloat(key, Float.parseFloat(value));
+                break;
+            case "quantity":
+                bundlePros.putInt(key, Integer.parseInt(value));
+                break;
+            default:
+                bundlePros.putString(key, value);
+        }
+    }
+
     public void onDeleteButtonClick(View view) {
         View v = (View) view.getParent();
         TextView itemID = (TextView) v.findViewById(R.id.item_id);
diff --git a/samples/TestApp/src/main/java/com/example/ui/home/HomeFragment.java b/samples/TestApp/src/main/java/com/example/ui/home/HomeFragment.java
index af8feaa..11550e2 100644
--- a/samples/TestApp/src/main/java/com/example/ui/home/HomeFragment.java
+++ b/samples/TestApp/src/main/java/com/example/ui/home/HomeFragment.java
@@ -14,7 +14,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
@@ -22,10 +21,6 @@
 
 import com.android.billingclient.api.*;
 import com.example.R;
-import com.tiktok.TikTokBusinessSdk;
-import com.tiktok.appevents.TTPurchaseInfo;
-
-import org.json.JSONObject;
 
 import java.util.*;
 
@@ -37,17 +32,23 @@ public class HomeFragment extends Fragment {
     private BillingClient billingClient;
     private SkuDetails skuDetails = null;
     private Purchase purchase = null;
+    private SkuDetails subsSkuDetails = null;
+    private Purchase subscribe = null;
 
     public View onCreateView(@NonNull LayoutInflater inflater,
                              ViewGroup container, Bundle savedInstanceState) {
         homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
         View root = inflater.inflate(R.layout.fragment_home, container, false);
         final TextView textView = root.findViewById(R.id.text_home);
+        final TextView subsTextView = root.findViewById(R.id.text_subs);
+        final TextView textLog = root.findViewById(R.id.text_log);
         homeViewModel.getText().observe(getViewLifecycleOwner(), s -> textView.setText(s));
+        homeViewModel.getSubscribeText().observe(getViewLifecycleOwner(), s -> subsTextView.setText(s));
+        homeViewModel.getLogText().observe(getViewLifecycleOwner(), s -> textLog.setText(s));
 
         textView.setOnClickListener(v -> {
             if (purchase != null) {
-                consumePurchase();
+                consumePurchase(true);
             } else if (skuDetails != null) {
                 Activity activity = requireActivity();
                 BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
@@ -55,9 +56,25 @@ public View onCreateView(@NonNull LayoutInflater inflater,
                         .build();
 
                 int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();
-                homeViewModel.setText("BillingResponseCode: " + responseCode);
+                homeViewModel.setLogText("BillingResponseCode: " + responseCode);
             } else {
-                queryPurchase();
+                queryPurchase(true);
+            }
+        });
+
+        subsTextView.setOnClickListener(v -> {
+            if (subscribe != null) {
+                consumePurchase(false);
+            } else if (subsSkuDetails != null) {
+                Activity activity = requireActivity();
+                BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
+                        .setSkuDetails(subsSkuDetails)
+                        .build();
+
+                int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();
+                homeViewModel.setLogText("BillingResponseCode: " + responseCode);
+            } else {
+                queryPurchase(false);
             }
         });
 
@@ -67,23 +84,23 @@ public View onCreateView(@NonNull LayoutInflater inflater,
                     && purchases != null) {
 
                 /** tiktok.monitor track purchase */
-                List purchaseInfos = new ArrayList<>();
-
-                try {
-                    for (Purchase purchase : purchases) {
-                        purchaseInfos.add(new TTPurchaseInfo(new JSONObject(purchase.getOriginalJson()), new JSONObject(skuDetails.getOriginalJson())));
-                    }
-                    TikTokBusinessSdk.trackGooglePlayPurchase(purchaseInfos);
-                } catch (Exception e) {
-                    Toast.makeText(HomeFragment.this.getActivity(), "Failed to track purchase: " + e.getMessage(), Toast.LENGTH_SHORT).show();
-                }
-
-                purchase = purchases.get(0);
-                homeViewModel.setText("purchase success, sku: " + purchase.getSku() + ". click to consume");
+//                List purchaseInfos = new ArrayList<>();
+//
+//                try {
+//                    for (Purchase purchase : purchases) {
+//                        purchaseInfos.add(new TTPurchaseInfo(new JSONObject(purchase.getOriginalJson()), new JSONObject(skuDetails.getOriginalJson())));
+//                    }
+//                    TikTokBusinessSdk.trackGooglePlayPurchase(purchaseInfos);
+//                } catch (Exception e) {
+//                    Toast.makeText(HomeFragment.this.getActivity(), "Failed to track purchase: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+//                }
+//
+//                purchase = purchases.get(0);
+//                homeViewModel.setText("purchase success, sku: " + purchase.getSkus().get(0) + ". click to consume");
             } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
-                homeViewModel.setText("USER_CANCELED");
+                homeViewModel.setLogText("USER_CANCELED");
             } else {
-                homeViewModel.setText("otherErr : " + billingResult.getResponseCode());
+                homeViewModel.setLogText("otherErr : " + billingResult.getResponseCode());
             }
         };
 
@@ -97,50 +114,77 @@ public View onCreateView(@NonNull LayoutInflater inflater,
         return root;
     }
 
-    private void newPurchase() {
+    private void newPurchase(boolean isInApp) {
         List skuList = new ArrayList<>();
-        skuList.add("android.test.purchased");
+        if(isInApp){
+            skuList.add("test_iap_item_1");
+        }else {
+            skuList.add("test_sub_item_2");
+        }
         SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
-        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
+        params.setSkusList(skuList).setType(isInApp?BillingClient.SkuType.INAPP:BillingClient.SkuType.SUBS);
         billingClient.querySkuDetailsAsync(params.build(), (billingResult1, skuDetailsList) -> {
             if (billingResult1.getResponseCode() == BillingClient.BillingResponseCode.OK
                     && skuDetailsList != null) {
 
                 if (skuDetailsList.size() > 0) {
-                    skuDetails = skuDetailsList.get(0);
-                    homeViewModel.setText("launchBillingFlow: " + skuDetails.getSku());
+                    if(isInApp){
+                        skuDetails = skuDetailsList.get(0);
+                    }else {
+                        subsSkuDetails = skuDetailsList.get(0);
+                    }
+                    getActivity().runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            homeViewModel.setLogText("launchBillingFlow: " + skuDetailsList.get(0).getSku());
+                        }
+                    });
                 }
             }
         });
     }
 
-    private void consumePurchase() {
+    private void consumePurchase(boolean isInApp) {
         ConsumeParams consumeParams = ConsumeParams.newBuilder()
-                .setPurchaseToken(purchase.getPurchaseToken())
+                .setPurchaseToken(isInApp ? purchase.getPurchaseToken() : subscribe.getPurchaseToken())
                 .build();
         ConsumeResponseListener listener = (billingResult, purchaseToken) -> {
             if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
-                purchase = null;
-                skuDetails = null;
-                homeViewModel.setText("Consume success. click to start sku fetch");
+                if(isInApp){
+                    purchase = null;
+                    skuDetails = null;
+                }else {
+                    subscribe = null;
+                    subsSkuDetails = null;
+                }
+                homeViewModel.setLogText("Consume success. click to start sku fetch");
             }
         };
 
         billingClient.consumeAsync(consumeParams, listener);
     }
 
-    private void queryPurchase() {
-        Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
-        if (purchasesResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
-            if (Objects.requireNonNull(purchasesResult.getPurchasesList()).size() > 0) {
-                // purchase exist
-                purchase = purchasesResult.getPurchasesList().get(0);
-                homeViewModel.setText("purchase exist, sku: " + purchase.getSku() + ". click to consume");
-            } else {
-                // new purchase
-                newPurchase();
+    private void queryPurchase(boolean isInApp) {
+        billingClient.queryPurchasesAsync(isInApp?BillingClient.SkuType.INAPP:BillingClient.SkuType.SUBS, new PurchasesResponseListener() {
+            @Override
+            public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List list) {
+                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+                    if (Objects.requireNonNull(list.size() > 0)) {
+                        // purchase exist
+                        if(isInApp){
+                            purchase = list.get(0);
+                        } else {
+                            subscribe = list.get(0);
+                        }
+                        homeViewModel.setLogText("purchase exist, sku: " + purchase.getSkus().get(0) + ". click to consume");
+                    } else {
+                        // new purchase
+                        newPurchase(isInApp);
+                    }
+                }
             }
-        }
+        });
+
     }
 
     private void startBilling() {
@@ -148,17 +192,17 @@ private void startBilling() {
             @Override
             public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
                 if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
-                    homeViewModel.setText("onBillingSetupFinished");
+                    homeViewModel.setLogText("onBillingSetupFinished");
                     // query existing purchases
-                    queryPurchase();
+                    queryPurchase(true);
                 } else {
-                    homeViewModel.setText("Failed to set up billing");
+                    homeViewModel.setLogText("Failed to set up billing");
                 }
             }
 
             @Override
             public void onBillingServiceDisconnected() {
-                homeViewModel.setText("onBillingServiceDisconnected");
+                homeViewModel.setLogText("onBillingServiceDisconnected");
                 new Handler(Looper.getMainLooper()).postDelayed(() -> startBilling(), 5000);
             }
         });
diff --git a/samples/TestApp/src/main/java/com/example/ui/home/HomeViewModel.java b/samples/TestApp/src/main/java/com/example/ui/home/HomeViewModel.java
index 8a06911..94e5a52 100644
--- a/samples/TestApp/src/main/java/com/example/ui/home/HomeViewModel.java
+++ b/samples/TestApp/src/main/java/com/example/ui/home/HomeViewModel.java
@@ -19,12 +19,18 @@
 public class HomeViewModel extends AndroidViewModel {
 
     private final MutableLiveData mText;
+    private final MutableLiveData mSubsText;
+    private final MutableLiveData mLogText;
     SharedPreferences sharedPreferences;
 
     public HomeViewModel(Application application) {
         super(application);
         mText = new MutableLiveData<>();
         mText.setValue("Purchase");
+        mSubsText = new MutableLiveData<>();
+        mSubsText.setValue("Subscribe");
+        mLogText = new MutableLiveData<>();
+        mLogText.setValue("init");
         sharedPreferences = getApplication().getSharedPreferences("TT_IDENTIFY", Context.MODE_PRIVATE);
     }
 
@@ -43,6 +49,22 @@ public LiveData getText() {
         return mText;
     }
 
+    public LiveData getSubscribeText() {
+        return mSubsText;
+    }
+
+    public void setSubscribeText(String txt) {
+        mSubsText.setValue(txt);
+    }
+
+    public LiveData getLogText() {
+        return mLogText;
+    }
+
+    public void setLogText(String txt) {
+        mLogText.setValue(txt);
+    }
+
     public void setNewCache(String externalId,
                             @Nullable String externalUserName,
                             @Nullable String phoneNumber,
diff --git a/samples/TestApp/src/main/java/com/example/ui/init/InitFragment.java b/samples/TestApp/src/main/java/com/example/ui/init/InitFragment.java
new file mode 100644
index 0000000..239ed54
--- /dev/null
+++ b/samples/TestApp/src/main/java/com/example/ui/init/InitFragment.java
@@ -0,0 +1,187 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.example.ui.init;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Switch;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.example.R;
+import com.example.ui.home.HomeViewModel;
+import com.tiktok.TikTokBusinessSdk;
+
+public class InitFragment extends Fragment {
+
+    private InitViewModel initViewModel;
+    private EditText appId;
+    private EditText ttAppId;
+    private Button init;
+    private Button startTrack;
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public View onCreateView(@NonNull LayoutInflater inflater,
+                             ViewGroup container, Bundle savedInstanceState) {
+
+        initViewModel = new ViewModelProvider(this).get(InitViewModel.class);
+        View root = inflater.inflate(R.layout.fragment_init, container, false);
+        appId = root.findViewById(R.id.app_id);
+        ttAppId = root.findViewById(R.id.tt_app_id);
+        init = root.findViewById(R.id.init);
+        startTrack = root.findViewById(R.id.startTrack);
+        ((Switch)(root.findViewById(R.id.autostart_status))).setChecked(InitViewModel.autoStart);
+        ((Switch)(root.findViewById(R.id.auto_events_status))).setChecked(InitViewModel.autoEvent);
+        ((Switch)(root.findViewById(R.id.install_logging_status))).setChecked(InitViewModel.loggingStatus);
+        ((Switch)(root.findViewById(R.id.launch_logging_status))).setChecked(InitViewModel.launchStatus);
+        ((Switch)(root.findViewById(R.id.retention_logging_status))).setChecked(InitViewModel.retentionStatus);
+        ((Switch)(root.findViewById(R.id.id_collection_status))).setChecked(InitViewModel.advertiserIDCollectionEnable);
+        ((Switch)(root.findViewById(R.id.monitor_status))).setChecked(InitViewModel.Metrics);
+        ((Switch)(root.findViewById(R.id.debug_status))).setChecked(InitViewModel.debugModeSwitch);
+        ((Switch)(root.findViewById(R.id.limited_status))).setChecked(InitViewModel.lduModeSwitch);
+        ((Switch)(root.findViewById(R.id.iap_status))).setChecked(InitViewModel.autoIapTrack);
+        init.setOnClickListener(v -> {
+            HomeViewModel homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
+
+            if (savedInstanceState == null) {
+                // !!!!!!!!!!!!!!!!!!!!!!!!!
+                // in order for this app to be runnable, plz create a resource file containing the relevant string resources
+                // Tiktok sdk init start
+                TikTokBusinessSdk.LogLevel logLevel = TikTokBusinessSdk.LogLevel.DEBUG;
+                TikTokBusinessSdk.TTConfig ttConfig = new TikTokBusinessSdk.TTConfig(getActivity().getApplicationContext());
+                try{
+                    int t = Integer.parseInt(String.valueOf(((EditText)(root.findViewById(R.id.level_label_et))).getText()));
+                    switch (t){
+                        case 0:
+                            logLevel = TikTokBusinessSdk.LogLevel.NONE;
+                            break;
+                        case 1:
+                            logLevel = TikTokBusinessSdk.LogLevel.INFO;
+                            break;
+                        case 2:
+                            logLevel = TikTokBusinessSdk.LogLevel.WARN;
+                            break;
+                        case 3:
+                            logLevel = TikTokBusinessSdk.LogLevel.DEBUG;
+                            break;
+
+                    }
+                }catch (Throwable throwable){
+                    throwable.printStackTrace();
+                }finally {
+                    ttConfig.setLogLevel(logLevel);
+                }
+                try{
+                    int t = Integer.parseInt(String.valueOf(((EditText)(root.findViewById(R.id.flush_time_et))).getText()));
+                    ttConfig.setFlushTimeInterval(t);
+                }catch (Throwable throwable){
+                    throwable.printStackTrace();
+                }
+                if(!((Switch)(root.findViewById(R.id.autostart_status))).isChecked()){
+                    ttConfig.disableAutoStart();
+                    InitViewModel.autoStart = false;
+                }else {
+                    InitViewModel.autoStart = true;
+                }
+                if(!((Switch)(root.findViewById(R.id.auto_events_status))).isChecked()){
+                    ttConfig.disableAutoEvents();
+                    InitViewModel.autoEvent = false;
+                }else {
+                    InitViewModel.autoEvent = true;
+                }
+                if(!((Switch)(root.findViewById(R.id.install_logging_status))).isChecked()){
+                    ttConfig.disableInstallLogging();
+                    InitViewModel.loggingStatus = false;
+                }else {
+                    InitViewModel.loggingStatus = true;
+                }
+                if(!((Switch)(root.findViewById(R.id.launch_logging_status))).isChecked()){
+                    ttConfig.disableLaunchLogging();
+                    InitViewModel.launchStatus = false;
+                }else {
+                    InitViewModel.launchStatus = true;
+                }
+                if(!((Switch)(root.findViewById(R.id.retention_logging_status))).isChecked()){
+                    ttConfig.disableRetentionLogging();
+                    InitViewModel.retentionStatus = false;
+                }else {
+                    InitViewModel.retentionStatus = true;
+                }
+                if(!((Switch)(root.findViewById(R.id.id_collection_status))).isChecked()){
+                    ttConfig.disableAdvertiserIDCollection();
+                    InitViewModel.advertiserIDCollectionEnable = false;
+                }else {
+                    InitViewModel.advertiserIDCollectionEnable = true;
+                }
+                if(!((Switch)(root.findViewById(R.id.monitor_status))).isChecked()){
+                    ttConfig.disableMonitor();
+                    InitViewModel.Metrics = false;
+                }else {
+                    InitViewModel.Metrics = true;
+                }
+                if(((Switch)(root.findViewById(R.id.debug_status))).isChecked()){
+                    ttConfig.openDebugMode();
+                    InitViewModel.debugModeSwitch = true;
+                } else {
+                    InitViewModel.debugModeSwitch = false;
+                }
+                if(((Switch)(root.findViewById(R.id.limited_status))).isChecked()){
+                    ttConfig.enableLimitedDataUse();
+                    InitViewModel.lduModeSwitch = true;
+                } else {
+                    InitViewModel.lduModeSwitch = false;
+                }
+                if(((Switch)(root.findViewById(R.id.iap_status))).isChecked()){
+                    ttConfig.enableAutoIapTrack();
+                    InitViewModel.autoIapTrack = true;
+                }else {
+                    InitViewModel.autoIapTrack = false;
+                }
+                if(appId.getText().toString() == null || appId.getText().toString().isEmpty()){
+                    ttConfig.setAppId("com.tiktok.iabtest");
+                } else {
+                    ttConfig.setAppId(appId.getText().toString());
+                }
+                if(ttAppId.getText().toString() == null || ttAppId.getText().toString().isEmpty()){
+                    ttConfig.setTTAppId("123456");
+                } else {
+                    ttConfig.setTTAppId(ttAppId.getText().toString());
+                }
+                TikTokBusinessSdk.initializeSdk(ttConfig);
+
+                // check if user info is cached & init
+                // homeViewModel.checkInitTTAM();
+
+                TikTokBusinessSdk.setOnCrashListener((thread, ex) -> android.util.Log.i("TikTokBusinessSdk", "setOnCrashListener" + thread.getName(), ex));
+
+                // testing delay tracking, implementing a 6 sec delay manually
+                // ideally has to be after accepting tracking permission
+                if(InitViewModel.autoStart) {
+                    new Handler(Looper.getMainLooper()).postDelayed(TikTokBusinessSdk::startTrack, 10000);
+                }
+            }
+        });
+        startTrack.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                TikTokBusinessSdk.startTrack();
+            }
+        });
+        return root;
+    }
+
+}
\ No newline at end of file
diff --git a/samples/TestApp/src/main/java/com/example/ui/init/InitViewModel.java b/samples/TestApp/src/main/java/com/example/ui/init/InitViewModel.java
new file mode 100644
index 0000000..d16a432
--- /dev/null
+++ b/samples/TestApp/src/main/java/com/example/ui/init/InitViewModel.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (c) 2023. Tiktok Inc.
+ *
+ * This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree.
+ ******************************************************************************/
+
+package com.example.ui.init;
+
+import android.app.Application;
+import androidx.lifecycle.AndroidViewModel;
+
+public class InitViewModel extends AndroidViewModel {
+
+    public static boolean autoEvent = true;
+    public static boolean advertiserIDCollectionEnable = true;
+    public static boolean autoStart = true;
+    public static boolean Metrics = true;
+    public static boolean debugModeSwitch = true;
+    public static boolean lduModeSwitch = false;
+    public static boolean autoIapTrack = true;
+    public static boolean loggingStatus = true;
+    public static boolean launchStatus = true;
+    public static boolean retentionStatus = true;
+
+    public InitViewModel(Application application) {
+        super(application);
+    }
+
+
+}
\ No newline at end of file
diff --git a/samples/TestApp/src/main/res/layout/activity_prop_editor.xml b/samples/TestApp/src/main/res/layout/activity_prop_editor.xml
index bc0eaf0..d461d7e 100644
--- a/samples/TestApp/src/main/res/layout/activity_prop_editor.xml
+++ b/samples/TestApp/src/main/res/layout/activity_prop_editor.xml
@@ -7,10 +7,14 @@
 
 
 
-    
+    
 
         
 
     
+    
 
 
diff --git a/samples/TestApp/src/main/res/layout/fragment_events.xml b/samples/TestApp/src/main/res/layout/fragment_events.xml
index 0ba1654..5740636 100644
--- a/samples/TestApp/src/main/res/layout/fragment_events.xml
+++ b/samples/TestApp/src/main/res/layout/fragment_events.xml
@@ -96,8 +96,57 @@
         android:layout_height="match_parent"
         android:gravity="bottom"
         tools:ignore="MissingConstraints">
+        
+            
+