Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-2210] Add support for Meta Install Referrer #1160

Merged
merged 15 commits into from
Mar 6, 2024
Merged
7 changes: 7 additions & 0 deletions Branch-SDK/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0" >
<queries>
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
<package android:name="com.facebook.katana" />
</queries>

<queries>
<package android:name="com.instagram.android" />
</queries>
</manifest>
140 changes: 135 additions & 5 deletions Branch-SDK/src/main/java/io/branch/coroutines/InstallReferrers.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
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
import io.branch.referral.util.classExists
import io.branch.referral.util.huaweiInstallReferrerClass
import io.branch.referral.util.samsungInstallReferrerClass
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

suspend fun getGooglePlayStoreReferrerDetails(context: Context): InstallReferrerResult? {
return withContext(Dispatchers.Default) {
Expand Down Expand Up @@ -230,6 +230,98 @@ suspend fun getXiaomiGetAppsReferrerDetails(context: Context): InstallReferrerRe
}
}

suspend fun getMetaInstallReferrerDetails(context: Context): InstallReferrerResult? = withContext(Dispatchers.Default) {
try {
val fbAppID = context.getString(
context.resources.getIdentifier("facebook_app_id", "string", context.packageName)
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
)
if (fbAppID.isEmpty()) {
BranchLogger.d("No Facebook App ID found in strings.xml. 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
return if (facebookResult != null && instagramResult != null) {
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
if (facebookResult.latestClickTimestamp > instagramResult.latestClickTimestamp) {
facebookResult
} else {
instagramResult
}
} else {
facebookResult ?: instagramResult
}
}

private fun queryProvider(context: Context, provider: String): InstallReferrerResult? {
val projection = arrayOf("install_referrer", "is_ct", "actual_timestamp")
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved

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")
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
return null
}

val timestampIndex = cursor.getColumnIndex("actual_timestamp")
val clickThroughIndex = cursor.getColumnIndex("is_ct")
val referrerIndex = cursor.getColumnIndex("install_referrer")

if (timestampIndex == -1 || clickThroughIndex == -1 || referrerIndex == -1) {
BranchLogger.d("getMetaInstallReferrerDetails - Required column not found in cursor for provider $provider")
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
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.e("getMetaInstallReferrerDetails - Error decoding URL: $e")
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
return null
}

if (utmContentValue.isEmpty()) {
BranchLogger.d("getMetaInstallReferrerDetails - utm_content is empty for provider $provider")
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
return null
}

BranchLogger.i("getMetaInstallReferrerDetails - Got Meta Install Referrer from provider $provider: $installReferrerString")

return try {
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
val json = JSONObject(utmContentValue)
val latestInstallTimestamp = json.getLong("t")

InstallReferrerResult(
Jsonkey.Meta_Install_Referrer.key,
latestInstallTimestamp,
installReferrerString,
actualTimestamp,
isClickThrough
)
} catch (e: JSONException) {
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
BranchLogger.e("getMetaInstallReferrerDetails - JSONException in queryProvider: $e")
null
}
}

return null
}

/**
* Invokes the source install referrer's coroutines in parallel.
* Await all and then do list operations
Expand All @@ -240,8 +332,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<InstallReferrerResult?> = listOf(googleReferrer.await(), huaweiReferrer.await(), samsungReferrer.await(), xiaomiReferrer.await())
val allReferrers: List<InstallReferrerResult?> = listOf(googleReferrer.await(), huaweiReferrer.await(), samsungReferrer.await(), xiaomiReferrer.await(), metaReferrer.await())
val latestReferrer = getLatestValidReferrerStore(allReferrers)

latestReferrer
Expand All @@ -258,5 +351,42 @@ fun getLatestValidReferrerStore(allReferrers: List<InstallReferrerResult?>): 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
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
private fun handleMetaInstallReferrer(allReferrers: List<InstallReferrerResult?>, latestReferrer: InstallReferrerResult): 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
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
}
}

return 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 }
return if (googleReferrer?.latestClickTimestamp == 0L) {
metaReferrer
} else {
val referrersWithoutMeta = allReferrers.filterNotNull().filterNot { it.appStore == Jsonkey.Meta_Install_Referrer.key }
referrersWithoutMeta.maxByOrNull {
it.latestInstallTimestamp
}
}
}

}
nsingh-branch marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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)

This file was deleted.

3 changes: 2 additions & 1 deletion Branch-SDK/src/main/java/io/branch/referral/Defines.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ public enum Jsonkey {
Google_Play_Store("PlayStore"),
Huawei_App_Gallery("AppGallery"),
Samsung_Galaxy_Store("GalaxyStore"),
Xiaomi_Get_Apps("GetApps");
Xiaomi_Get_Apps("GetApps"),
Meta_Install_Referrer("Meta");

private final String key;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<InstallReferrerResult> installReferrers = new ArrayList<>();
Expand Down Expand Up @@ -77,7 +82,8 @@ public void oneValidReferrerReturnsItself(){
"test1",
1,
"test1",
1
1,
true
);

List<InstallReferrerResult> installReferrers = new ArrayList<>();
Expand All @@ -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<InstallReferrerResult> 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);
}

}
Loading