diff --git a/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/CustomBranchApp.java b/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/CustomBranchApp.java index 2d87e1216..e29ba9713 100644 --- a/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/CustomBranchApp.java +++ b/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/CustomBranchApp.java @@ -10,6 +10,7 @@ public final class CustomBranchApp extends Application { @Override public void onCreate() { super.onCreate(); + IBranchLoggingCallbacks iBranchLoggingCallbacks = new IBranchLoggingCallbacks() { @Override public void onBranchLog(String logMessage, String severityConstantName) { @@ -17,6 +18,7 @@ public void onBranchLog(String logMessage, String severityConstantName) { } }; Branch.enableLogging(iBranchLoggingCallbacks); + Branch.getAutoInstance(this); } } diff --git a/Branch-SDK/src/main/java/io/branch/coroutines/InstallReferrers.kt b/Branch-SDK/src/main/java/io/branch/coroutines/InstallReferrers.kt index 63944c41c..16376a6fe 100644 --- a/Branch-SDK/src/main/java/io/branch/coroutines/InstallReferrers.kt +++ b/Branch-SDK/src/main/java/io/branch/coroutines/InstallReferrers.kt @@ -1,12 +1,11 @@ package io.branch.coroutines import android.content.Context +import android.net.Uri import android.os.RemoteException -import android.util.Log import com.android.installreferrer.api.InstallReferrerClient import com.android.installreferrer.api.InstallReferrerStateListener import io.branch.data.InstallReferrerResult -import io.branch.referral.AppStoreReferrer import io.branch.referral.BranchLogger import io.branch.referral.Defines.Jsonkey import io.branch.referral.PrefHelper @@ -17,9 +16,15 @@ import io.branch.referral.util.xiaomiInstallReferrerClass import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext +import org.json.JSONException +import org.json.JSONObject +import java.net.URLDecoder + +private const val installReferrer = "install_referrer" +private const val isCt = "is_ct" +private const val actualTimestamp = "actual_timestamp" suspend fun getGooglePlayStoreReferrerDetails(context: Context): InstallReferrerResult? { return withContext(Dispatchers.Default) { @@ -230,6 +235,101 @@ suspend fun getXiaomiGetAppsReferrerDetails(context: Context): InstallReferrerRe } } +suspend fun getMetaInstallReferrerDetails(context: Context): InstallReferrerResult? = withContext(Dispatchers.Default) { + try { + val fbAppID = PrefHelper.fbAppId_ + + if (fbAppID.isNullOrEmpty()) { + BranchLogger.d("No Facebook App ID provided. Can't check for Meta Install Referrer") + null + } else { + queryMetaInstallReferrer(context, fbAppID) + } + } catch (exception: Exception) { + BranchLogger.e("Exception in getMetaInstallReferrerDetails: $exception") + null + } +} +private fun queryMetaInstallReferrer(context: Context, fbAppId: String): InstallReferrerResult? { + val facebookProvider = "content://com.facebook.katana.provider.InstallReferrerProvider/$fbAppId" + val instagramProvider = "content://com.instagram.contentprovider.InstallReferrerProvider/$fbAppId" + + val facebookResult = queryProvider(context, facebookProvider) + val instagramResult = queryProvider(context, instagramProvider) + + // Check both Facebook and Instagram for install referrers and return the latest one + val result: InstallReferrerResult? + + if (facebookResult != null && instagramResult != null) { + if (facebookResult.latestClickTimestamp > instagramResult.latestClickTimestamp) { + result = facebookResult + } else { + result = instagramResult + } + } else { + result = facebookResult ?: instagramResult + } + + return result +} + +private fun queryProvider(context: Context, provider: String): InstallReferrerResult? { + val projection = arrayOf(installReferrer, isCt, actualTimestamp) + + context.contentResolver.query(Uri.parse(provider), projection, null, null, null)?.use { cursor -> + + if (!cursor.moveToFirst()) { + BranchLogger.d("getMetaInstallReferrerDetails - cursor is empty or null for provider $provider") + return null + } + + val timestampIndex = cursor.getColumnIndex(actualTimestamp) + val clickThroughIndex = cursor.getColumnIndex(isCt) + val referrerIndex = cursor.getColumnIndex(installReferrer) + + if (timestampIndex == -1 || clickThroughIndex == -1 || referrerIndex == -1) { + BranchLogger.w("getMetaInstallReferrerDetails - Required column not found in cursor for provider $provider") + return null + } + + val actualTimestamp = cursor.getLong(timestampIndex) + val isClickThrough = cursor.getInt(clickThroughIndex) == 1 + val installReferrerString = cursor.getString(referrerIndex) + + val utmContentValue = try { + URLDecoder.decode(installReferrerString, "UTF-8").substringAfter("utm_content=", "") + } catch (e: IllegalArgumentException) { + BranchLogger.w("getMetaInstallReferrerDetails - Error decoding URL: $e") + return null + } + + if (utmContentValue.isEmpty()) { + BranchLogger.w("getMetaInstallReferrerDetails - utm_content is empty for provider $provider") + return null + } + + BranchLogger.i("getMetaInstallReferrerDetails - Got Meta Install Referrer from provider $provider: $installReferrerString") + + try { + val json = JSONObject(utmContentValue) + val latestInstallTimestamp = json.getLong("t") + + return InstallReferrerResult( + Jsonkey.Meta_Install_Referrer.key, + latestInstallTimestamp, + installReferrerString, + actualTimestamp, + isClickThrough + ) + } catch (e: JSONException) { + BranchLogger.w("getMetaInstallReferrerDetails - JSONException in queryProvider: $e") + return null + } + } + + return null +} + /** * Invokes the source install referrer's coroutines in parallel. * Await all and then do list operations @@ -240,8 +340,9 @@ suspend fun fetchLatestInstallReferrer(context: Context): InstallReferrerResult? val huaweiReferrer = async { getHuaweiAppGalleryReferrerDetails(context) } val samsungReferrer = async { getSamsungGalaxyStoreReferrerDetails(context) } val xiaomiReferrer = async { getXiaomiGetAppsReferrerDetails(context) } + val metaReferrer = async { getMetaInstallReferrerDetails(context) } - val allReferrers: List = listOf(googleReferrer.await(), huaweiReferrer.await(), samsungReferrer.await(), xiaomiReferrer.await()) + val allReferrers: List = listOf(googleReferrer.await(), huaweiReferrer.await(), samsungReferrer.await(), xiaomiReferrer.await(), metaReferrer.await()) val latestReferrer = getLatestValidReferrerStore(allReferrers) latestReferrer @@ -258,5 +359,44 @@ fun getLatestValidReferrerStore(allReferrers: List): Ins it.latestInstallTimestamp } + if (allReferrers.filterNotNull().any { it.appStore == Jsonkey.Meta_Install_Referrer.key }) { + val latestReferrer = handleMetaInstallReferrer(allReferrers, result!!) + if (latestReferrer?.appStore == Jsonkey.Meta_Install_Referrer.key) { + latestReferrer?.appStore = Jsonkey.Google_Play_Store.key + } + return latestReferrer + } + + return result +} + +//Handle the deduplication and click vs view logic for Meta install referrer +private fun handleMetaInstallReferrer(allReferrers: List, latestReferrer: InstallReferrerResult): InstallReferrerResult? { + val result: InstallReferrerResult? + val metaReferrer = allReferrers.filterNotNull().firstOrNull { it.appStore == Jsonkey.Meta_Install_Referrer.key } + + if (metaReferrer!!.isClickThrough) { + //The Meta Referrer is click through. Return it if it or the matching Play Store referrer is the latest + if (latestReferrer.appStore == Jsonkey.Google_Play_Store.key) { + //Deduplicate the Meta and Play Store referrers + if (latestReferrer.latestClickTimestamp == metaReferrer.latestClickTimestamp) { + return metaReferrer + } + } + + result = latestReferrer + } else { + //The Meta Referrer is view through. Return it if the Play Store referrer is organic (latestClickTimestamp is 0) + val googleReferrer = allReferrers.filterNotNull().firstOrNull { it.appStore == Jsonkey.Google_Play_Store.key } + if (googleReferrer?.latestClickTimestamp == 0L) { + result = metaReferrer + } else { + val referrersWithoutMeta = allReferrers.filterNotNull().filterNot { it.appStore == Jsonkey.Meta_Install_Referrer.key } + result = referrersWithoutMeta.maxByOrNull { + it.latestInstallTimestamp + } + } + } + return result } \ No newline at end of file diff --git a/Branch-SDK/src/main/java/io/branch/data/InstallReferrerResult.kt b/Branch-SDK/src/main/java/io/branch/data/InstallReferrerResult.kt index 39faedf37..53f6651f5 100644 --- a/Branch-SDK/src/main/java/io/branch/data/InstallReferrerResult.kt +++ b/Branch-SDK/src/main/java/io/branch/data/InstallReferrerResult.kt @@ -3,4 +3,5 @@ package io.branch.data data class InstallReferrerResult (var appStore: String?, var latestInstallTimestamp: Long, var latestRawReferrer: String?, - var latestClickTimestamp: Long) + var latestClickTimestamp: Long, + var isClickThrough: Boolean = true) diff --git a/Branch-SDK/src/main/java/io/branch/referral/Branch.java b/Branch-SDK/src/main/java/io/branch/referral/Branch.java index 17b5d099b..c295edbb1 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/Branch.java +++ b/Branch-SDK/src/main/java/io/branch/referral/Branch.java @@ -2472,4 +2472,8 @@ public void logEventWithPurchase(@NonNull Context context, @NonNull Purchase pur public static void useEUEndpoint() { PrefHelper.useEUEndpoint(true); } + + public static void setFBAppID(String fbAppID) { + PrefHelper.fbAppId_ = fbAppID; + } } diff --git a/Branch-SDK/src/main/java/io/branch/referral/DeferredAppLinkDataHandler.java b/Branch-SDK/src/main/java/io/branch/referral/DeferredAppLinkDataHandler.java deleted file mode 100644 index 2909b2ef6..000000000 --- a/Branch-SDK/src/main/java/io/branch/referral/DeferredAppLinkDataHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.branch.referral; - -import android.content.Context; -import android.os.Bundle; -import android.text.TextUtils; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -/** - * Created by sojanpr on 5/19/16. - * Class for handling deferred app links - */ -class DeferredAppLinkDataHandler { - private static final String NATIVE_URL_KEY = "com.facebook.platform.APPLINK_NATIVE_URL"; - - public static Boolean fetchDeferredAppLinkData(Context context, final AppLinkFetchEvents callback) { - boolean isRequestSucceeded = true; - try { - // Init FB SDK - Class FacebookSdkClass = Class.forName("com.facebook.FacebookSdk"); - Method initSdkMethod = FacebookSdkClass.getMethod("sdkInitialize", Context.class); - initSdkMethod.invoke(null, context); - - final Class AppLinkDataClass = Class.forName("com.facebook.applinks.AppLinkData"); - Class AppLinkDataCompletionHandlerClass = Class.forName("com.facebook.applinks.AppLinkData$CompletionHandler"); - Method fetchDeferredAppLinkDataMethod = AppLinkDataClass.getMethod("fetchDeferredAppLinkData", Context.class, String.class, AppLinkDataCompletionHandlerClass); - - InvocationHandler ALDataCompletionHandler = new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Exception { - if (method.getName().equals("onDeferredAppLinkDataFetched") && args[0] != null) { - String appLinkUrl = null; - Object appLinkDataClass = AppLinkDataClass.cast(args[0]); - Method getArgumentBundleMethod = AppLinkDataClass.getMethod("getArgumentBundle"); - Bundle appLinkDataBundle = Bundle.class.cast(getArgumentBundleMethod.invoke(appLinkDataClass)); - - if (appLinkDataBundle != null) { - appLinkUrl = appLinkDataBundle.getString(NATIVE_URL_KEY); - } - - if (callback != null) { - callback.onAppLinkFetchFinished(appLinkUrl); - } - - } else { - if (callback != null) { - callback.onAppLinkFetchFinished(null); - } - } - return null; - } - }; - - Object completionListenerInterface = Proxy.newProxyInstance(AppLinkDataCompletionHandlerClass.getClassLoader() - , new Class[]{AppLinkDataCompletionHandlerClass} - , ALDataCompletionHandler); - - String fbAppID = context.getString(context.getResources().getIdentifier("facebook_app_id", "string", context.getPackageName())); - if (TextUtils.isEmpty(fbAppID)) { - isRequestSucceeded = false; - } else { - fetchDeferredAppLinkDataMethod.invoke(null, context, fbAppID, completionListenerInterface); - } - - } catch (Exception ex) { - isRequestSucceeded = false; - } - return isRequestSucceeded; - } - - public interface AppLinkFetchEvents { - void onAppLinkFetchFinished(String nativeAppLinkUrl); - } -} diff --git a/Branch-SDK/src/main/java/io/branch/referral/Defines.java b/Branch-SDK/src/main/java/io/branch/referral/Defines.java index 5c2f9f9cd..42149d717 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/Defines.java +++ b/Branch-SDK/src/main/java/io/branch/referral/Defines.java @@ -218,6 +218,8 @@ public enum Jsonkey { Huawei_App_Gallery("AppGallery"), Samsung_Galaxy_Store("GalaxyStore"), Xiaomi_Get_Apps("GetApps"), + Meta_Install_Referrer("Meta"), + DMA_EEA("dma_eea"), DMA_Ad_Personalization("dma_ad_personalization"), DMA_Ad_User_Data("dma_ad_user_data"); diff --git a/Branch-SDK/src/main/java/io/branch/referral/PrefHelper.java b/Branch-SDK/src/main/java/io/branch/referral/PrefHelper.java index fba1766bb..8a1941e97 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/PrefHelper.java +++ b/Branch-SDK/src/main/java/io/branch/referral/PrefHelper.java @@ -1386,6 +1386,8 @@ boolean shouldAddModules () { private static boolean enableLogging_ = false; private static boolean useEUEndpoint_ = false; + public static String fbAppId_ = null; + static void enableLogging(boolean fEnable) { enableLogging_ = fEnable; } diff --git a/Branch-SDK/src/test/java/io/branch/referral/InstallReferrerResultTests.java b/Branch-SDK/src/test/java/io/branch/referral/InstallReferrerResultTests.java index ddaa7b52f..999bc7e24 100644 --- a/Branch-SDK/src/test/java/io/branch/referral/InstallReferrerResultTests.java +++ b/Branch-SDK/src/test/java/io/branch/referral/InstallReferrerResultTests.java @@ -8,6 +8,7 @@ import org.junit.runners.JUnit4; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import io.branch.coroutines.InstallReferrersKt; @@ -25,28 +26,32 @@ public void allValidReferrersLatestWins(){ "test1", 1, "test1", - 1 + 1, + true ); InstallReferrerResult test2 = new InstallReferrerResult( "test2", Long.MAX_VALUE, "test2", - Long.MAX_VALUE + Long.MAX_VALUE, + true ); InstallReferrerResult test3 = new InstallReferrerResult( "test3", 2, "test3", - 2 + 2, + true ); InstallReferrerResult test4 = new InstallReferrerResult( "test4", 3, "test4", - 3 + 3, + true ); List installReferrers = new ArrayList<>(); @@ -77,7 +82,8 @@ public void oneValidReferrerReturnsItself(){ "test1", 1, "test1", - 1 + 1, + true ); List installReferrers = new ArrayList<>(); @@ -86,4 +92,44 @@ public void oneValidReferrerReturnsItself(){ InstallReferrerResult actual = InstallReferrersKt.getLatestValidReferrerStore(installReferrers); Assert.assertEquals(test1, actual); } + + @Test + public void testMetaInstallReferrerCases() { + // Case 1: Meta referrer is click-through with non-organic Play Store referrer + InstallReferrerResult metaReferrerClickThrough = new InstallReferrerResult("Meta", 1700000050, "referrer", 1700000000, true); + InstallReferrerResult playStoreReferrer = new InstallReferrerResult("PlayStore", 1700000030, "utm_source=google-play&utm_medium=cpc", 1700000000, true); + List allReferrers = Arrays.asList(metaReferrerClickThrough, playStoreReferrer); + InstallReferrerResult result = InstallReferrersKt.getLatestValidReferrerStore(allReferrers); + Assert.assertEquals(metaReferrerClickThrough, result); + + // Case 2: Meta referrer is view-through with organic Play Store referrer + InstallReferrerResult metaReferrerViewThrough = new InstallReferrerResult("Meta", 1700000050, "referrer", 1700000000, false); + InstallReferrerResult latestPlayStoreReferrer = new InstallReferrerResult("PlayStore", 1700000030, "utm_source=google-play&utm_medium=organic", 0, true); + allReferrers = Arrays.asList(metaReferrerViewThrough, latestPlayStoreReferrer); + result = InstallReferrersKt.getLatestValidReferrerStore(allReferrers); + Assert.assertEquals(metaReferrerViewThrough, result); + + // Case 3: Meta referrer is view-through with non-organic Play Store referrer + metaReferrerViewThrough = new InstallReferrerResult("Meta", 1700000050, "referrer", 1700000000, false); + latestPlayStoreReferrer = new InstallReferrerResult("PlayStore", 1700000030, "utm_source=google-play&utm_medium=cpc", 1700000000, true); + allReferrers = Arrays.asList(metaReferrerViewThrough, latestPlayStoreReferrer); + result = InstallReferrersKt.getLatestValidReferrerStore(allReferrers); + Assert.assertEquals(latestPlayStoreReferrer, result); + + // Case 4: Meta referrer is outdated click-through with non-organic Play Store referrer + metaReferrerClickThrough = new InstallReferrerResult("Meta", 1700000030, "referrer", 1700000000, true); + latestPlayStoreReferrer = new InstallReferrerResult("PlayStore", 1700000500, "utm_source=google-play&utm_medium=cpc", 1700000450, true); + allReferrers = Arrays.asList(metaReferrerClickThrough, latestPlayStoreReferrer); + result = InstallReferrersKt.getLatestValidReferrerStore(allReferrers); + Assert.assertEquals(latestPlayStoreReferrer, result); + + // Case 5: Meta, Google Play, and Samsung Referrer (latest) are available + metaReferrerClickThrough = new InstallReferrerResult("Meta", 1700000000, "referrer", 1700000000, true); + latestPlayStoreReferrer = new InstallReferrerResult("PlayStore", 1700000000, "utm_source=google-play&utm_medium=cpc", 1700000000, true); + InstallReferrerResult samsungReferrer = new InstallReferrerResult("Samsung", 1700001000, "utm_source=samsung-store&utm_medium=cpc", 1700001000, true); + allReferrers = Arrays.asList(metaReferrerClickThrough, latestPlayStoreReferrer, samsungReferrer); + result = InstallReferrersKt.getLatestValidReferrerStore(allReferrers); + Assert.assertEquals(samsungReferrer, result); + } + }