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

3DS2 2.2 Device Data Requirements #9298

Merged
merged 8 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3ds2sdk/api/3ds2sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public abstract interface class com/stripe/android/stripe3ds2/init/ConfigParamet
}

public abstract interface class com/stripe/android/stripe3ds2/init/DeviceDataFactory {
public abstract fun create ()Ljava/util/Map;
public abstract fun create (Ljava/lang/String;Lcom/stripe/android/stripe3ds2/transaction/SdkTransactionId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class com/stripe/android/stripe3ds2/init/HardwareId$Creator : android/os/Parcelable$Creator {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.stripe.android.stripe3ds2.init

import com.stripe.android.stripe3ds2.transaction.SdkTransactionId

fun interface DeviceDataFactory {
fun create(): Map<String, Any?>
suspend fun create(sdkReferenceNumber: String, sdkTransactionId: SdkTransactionId): Map<String, Any?>
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package com.stripe.android.stripe3ds2.init

import android.annotation.TargetApi
import android.content.Context
import android.content.pm.PackageManager.FEATURE_TELEPHONY_IMS
import android.media.AudioManager
import android.os.Build
import android.provider.Settings
import android.telephony.TelephonyManager
import android.util.DisplayMetrics
import android.webkit.WebSettings
import androidx.core.os.LocaleListCompat
import com.stripe.android.stripe3ds2.utils.Supplier
import com.stripe.android.stripe3ds2.transaction.MessageVersionRegistry
import com.stripe.android.stripe3ds2.transaction.SdkTransactionId
import java.lang.reflect.Field
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone

Expand All @@ -14,33 +24,119 @@ import java.util.TimeZone
*/
internal class DeviceDataFactoryImpl internal constructor(
context: Context,
private val hardwareIdSupplier: Supplier<HardwareId>
private val appInfoRepository: AppInfoRepository,
private val messageVersionRegistry: MessageVersionRegistry,
) : DeviceDataFactory {
private val displayMetrics: DisplayMetrics = context.resources.displayMetrics
private val defaultUserAgent = WebSettings.getDefaultUserAgent(context)
private val telephonyManager =
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager)
private val secureFRPMode =
Settings.Secure.getInt(context.contentResolver, Settings.Global.SECURE_FRP_MODE, 0)
private val audioManager = (context.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
private val packageManager = context.packageManager
private val apiVersion = Build.VERSION.SDK_INT
private val dateFormat = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault())
private val dateTime = dateFormat.format(Calendar.getInstance().time)

override fun create(): Map<String, Any?> {
val hardwareId = hardwareIdSupplier.get().value
return mapOf(
private val codeName = buildCodeName() ?: "UNKNOWN"
private val osName = "Android " + codeName + " " + Build.VERSION.RELEASE + " API " + apiVersion
private val timeZone = TimeZone.getDefault().rawOffset / MILLIS_IN_SECOND / SECONDS_IN_MINUTE

@TargetApi(Build.VERSION_CODES.TIRAMISU)
override suspend fun create(
sdkReferenceNumber: String,
sdkTransactionId: SdkTransactionId
): Map<String, Any?> {
val map = hashMapOf(
DeviceParam.PARAM_PLATFORM.toString() to "Android",
DeviceParam.PARAM_DEVICE_MODEL.toString() to Build.MODEL,
DeviceParam.PARAM_OS_NAME.toString() to Build.VERSION.CODENAME,
DeviceParam.PARAM_DEVICE_MODEL.toString() to Build.MANUFACTURER + "||" + Build.MODEL,
DeviceParam.PARAM_OS_NAME.toString() to osName,
DeviceParam.PARAM_OS_VERSION.toString() to Build.VERSION.RELEASE,
DeviceParam.PARAM_LOCALE.toString() to
LocaleListCompat.create(Locale.getDefault()).toLanguageTags(),
DeviceParam.PARAM_TIME_ZONE.toString() to TimeZone.getDefault().displayName,
DeviceParam.PARAM_TIME_ZONE.toString() to timeZone.toString(),
DeviceParam.PARAM_SCREEN_RESOLUTION.toString() to
String.format(
Locale.ROOT,
"%sx%s",
displayMetrics.heightPixels,
displayMetrics.widthPixels
)
).plus(
if (hardwareId.isNotEmpty()) {
mapOf(DeviceParam.PARAM_HARDWARE_ID.toString() to hardwareId)
} else {
emptyMap()
}
),
DeviceParam.PARAM_SDK_APP_ID.toString() to appInfoRepository.get().sdkAppId,
DeviceParam.PARAM_SDK_VERSION.toString() to messageVersionRegistry.current,
DeviceParam.PARAM_SDK_REF_NUMBER.toString() to sdkReferenceNumber,
DeviceParam.PARAM_DATE_TIME.toString() to dateTime,
DeviceParam.PARAM_SDK_TRANS_ID.toString() to sdkTransactionId.toString(),
DeviceParam.PARAM_WEB_VIEW_USER_AGENT.toString() to defaultUserAgent,
)

if (apiVersion >= Build.VERSION_CODES.P) {
map[DeviceParam.PARAM_SIM_CARRIER_ID.toString()] =
telephonyManager.simCarrierId.toString()
map[DeviceParam.PARAM_SIM_CARRIER_ID_NAME.toString()] =
telephonyManager.simCarrierIdName.toString()
}

if (apiVersion >= Build.VERSION_CODES.Q) {
map[DeviceParam.PARAM_SIM_SPECIFIC_CARRIER_ID.toString()] =
telephonyManager.simSpecificCarrierId.toString()
map[DeviceParam.PARAM_SIM_SPECIFIC_CARRIER_ID_NAME.toString()] =
telephonyManager.simSpecificCarrierIdName.toString()

if (packageManager.hasSystemFeature(FEATURE_TELEPHONY_IMS)) {
map[DeviceParam.PARAM_RTT_CALLING_MODE.toString()] =
telephonyManager.isRttSupported.toString()
}
}

if (apiVersion >= Build.VERSION_CODES.R) {
map[DeviceParam.PARAM_SUBSCRIPTION_ID.toString()] =
telephonyManager.subscriptionId.toString()
map[DeviceParam.PARAM_SECURE_FRP_MODE.toString()] =
if (secureFRPMode == 1) "true" else "false"
}

if (apiVersion >= Build.VERSION_CODES.S) {
map[DeviceParam.PARAM_HARDWARE_SKU.toString()] =
Build.SKU
map[DeviceParam.PARAM_SOC_MANUFACTURER.toString()] =
Build.SOC_MANUFACTURER
map[DeviceParam.PARAM_SOC_MODEL.toString()] =
Build.SOC_MODEL
}

if (apiVersion >= Build.VERSION_CODES.TIRAMISU) {
map[DeviceParam.PARAM_APPLY_RAMPING_RINGER.toString()] =
audioManager.isRampingRingerEnabled.toString()
}

return map
}

private fun buildCodeName(): String? {
val fields: Array<Field> = Build.VERSION_CODES::class.java.fields
for (field in fields) {
val fieldName: String = field.name
var fieldValue = -1

try {
fieldValue = field.getInt(Any())
} catch (_: IllegalArgumentException) {
} catch (_: IllegalAccessException) {
} catch (_: NullPointerException) {
}

if (fieldValue == Build.VERSION.SDK_INT) {
return fieldName
}
}

return null
}

companion object {
private const val MILLIS_IN_SECOND = 1000
private const val SECONDS_IN_MINUTE = 60
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ internal enum class DeviceParam(private val code: String) {
PARAM_OS_VERSION("C004"),
PARAM_LOCALE("C005"),
PARAM_TIME_ZONE("C006"),
PARAM_HARDWARE_ID("C007"),
PARAM_SCREEN_RESOLUTION("C008"),
PARAM_DEVICE_NAME("C009"),
PARAM_IP_ADDRESS("C010"),
PARAM_LATITUDE("C011"),
PARAM_LONGITUDE("C012"),
PARAM_APP_PACKAGE_NAME("C013"),
PARAM_SDK_APP_ID("C014"),
PARAM_SDK_VERSION("C015"),
PARAM_SDK_REF_NUMBER("C016"),
PARAM_DATE_TIME("C017"),
PARAM_SDK_TRANS_ID("C018"),

// Android-Specific parameters

Expand Down Expand Up @@ -178,7 +183,30 @@ internal enum class DeviceParam(private val code: String) {
PARAM_DISPLAY_YDPI("A135"),

// StatFs parameters
PARAM_STAT_FS_TOTAL_BYTES("A136");
PARAM_STAT_FS_TOTAL_BYTES("A136"),

// Web View parameters
PARAM_WEB_VIEW_USER_AGENT("A137"),

// SIM parameters
PARAM_SIM_CARRIER_ID("A138"),
PARAM_SIM_CARRIER_ID_NAME("A139"),
PARAM_MANUFACTURER_CODE("A140"),
PARAM_SIM_SPECIFIC_CARRIER_ID("A141"),
PARAM_SIM_SPECIFIC_CARRIER_ID_NAME("A142"),
PARAM_MULTI_SIM_SUPPORTED("A143"),
PARAM_SUBSCRIPTION_ID("A145"),

PARAM_6GHZ_BAND_SUPPORTED("A146"),
PARAM_PASSPOINT_FQDN("A147"),
PARAM_PASSPOINT_PROVIDER_FRIENDLY_NAME("A148"),
PARAM_BONDED_DEVICES_ALIAS("A149"),
PARAM_RTT_CALLING_MODE("A150"),
PARAM_SECURE_FRP_MODE("A151"),
PARAM_APPLY_RAMPING_RINGER("A152"),
PARAM_HARDWARE_SKU("A153"),
PARAM_SOC_MANUFACTURER("A154"),
PARAM_SOC_MODEL("A155");

override fun toString(): String {
return code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.stripe.android.stripe3ds2.init

import android.os.Build
import androidx.annotation.VisibleForTesting
import com.stripe.android.stripe3ds2.utils.Supplier
import java.util.HashMap

/**
Expand All @@ -11,20 +10,17 @@ import java.util.HashMap
*/
internal class DeviceParamNotAvailableFactoryImpl internal constructor(
private val apiVersion: Int,
private val hardwareIdSupplier: Supplier<HardwareId>
) : DeviceParamNotAvailableFactory {

internal constructor(
hardwareIdSupplier: Supplier<HardwareId>
) : this(
internal constructor() : this(
Build.VERSION.SDK_INT,
hardwareIdSupplier
)

override fun create(): Map<String, String> {
return marketOrRegionRestrictionParams
.plus(platformVersionParams)
.plus(permissionParams)
.plus(unavailableParams)
}

@VisibleForTesting
Expand All @@ -39,8 +35,26 @@ internal class DeviceParamNotAvailableFactoryImpl internal constructor(
DeviceParam.PARAM_OS_VERSION,
DeviceParam.PARAM_LOCALE,
DeviceParam.PARAM_TIME_ZONE,
DeviceParam.PARAM_HARDWARE_ID,
DeviceParam.PARAM_SCREEN_RESOLUTION
DeviceParam.PARAM_SCREEN_RESOLUTION,
DeviceParam.PARAM_SDK_APP_ID,
DeviceParam.PARAM_SDK_VERSION,
DeviceParam.PARAM_SDK_REF_NUMBER,
DeviceParam.PARAM_DATE_TIME,
DeviceParam.PARAM_SDK_TRANS_ID,
DeviceParam.PARAM_WEB_VIEW_USER_AGENT,
DeviceParam.PARAM_SIM_CARRIER_ID,
DeviceParam.PARAM_SECURE_FRP_MODE,
DeviceParam.PARAM_APPLY_RAMPING_RINGER,
DeviceParam.PARAM_HARDWARE_SKU,
DeviceParam.PARAM_SOC_MANUFACTURER,
DeviceParam.PARAM_SOC_MODEL,
DeviceParam.PARAM_SIM_CARRIER_ID_NAME,
DeviceParam.PARAM_MANUFACTURER_CODE,
DeviceParam.PARAM_SIM_SPECIFIC_CARRIER_ID,
DeviceParam.PARAM_SIM_SPECIFIC_CARRIER_ID_NAME,
DeviceParam.PARAM_MULTI_SIM_SUPPORTED,
DeviceParam.PARAM_SUBSCRIPTION_ID,
DeviceParam.PARAM_RTT_CALLING_MODE
)

DeviceParam.entries.forEach {
Expand Down Expand Up @@ -97,6 +111,44 @@ internal class DeviceParamNotAvailableFactoryImpl internal constructor(
Reason.PLATFORM_VERSION.toString()
}

if (apiVersion < Build.VERSION_CODES.P) {
params[DeviceParam.PARAM_SIM_CARRIER_ID.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_SIM_CARRIER_ID_NAME.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_RTT_CALLING_MODE.toString()] =
Reason.PLATFORM_VERSION.toString()
}

if (apiVersion < Build.VERSION_CODES.Q) {
params[DeviceParam.PARAM_SIM_SPECIFIC_CARRIER_ID.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_SIM_SPECIFIC_CARRIER_ID_NAME.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_MULTI_SIM_SUPPORTED.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_APPLY_RAMPING_RINGER.toString()] =
Reason.PLATFORM_VERSION.toString()
}

if (apiVersion < Build.VERSION_CODES.R) {
params[DeviceParam.PARAM_SUBSCRIPTION_ID.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_BONDED_DEVICES_ALIAS.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_SECURE_FRP_MODE.toString()] =
Reason.PLATFORM_VERSION.toString()
}

if (apiVersion < Build.VERSION_CODES.S) {
params[DeviceParam.PARAM_HARDWARE_SKU.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_SOC_MANUFACTURER.toString()] =
Reason.PLATFORM_VERSION.toString()
params[DeviceParam.PARAM_SOC_MODEL.toString()] =
Reason.PLATFORM_VERSION.toString()
}

return params
}

Expand Down Expand Up @@ -131,10 +183,6 @@ internal class DeviceParamNotAvailableFactoryImpl internal constructor(
params[DeviceParam.PARAM_LATITUDE.toString()] = Reason.PERMISSION.toString()
params[DeviceParam.PARAM_LONGITUDE.toString()] = Reason.PERMISSION.toString()

if (!hardwareIdSupplier.get().isPresent) {
params[DeviceParam.PARAM_HARDWARE_ID.toString()] = Reason.PLATFORM_VERSION.toString()
}

params[DeviceParam.PARAM_DEVICE_NAME.toString()] =
Reason.PERMISSION.toString()
params[DeviceParam.PARAM_BLUETOOTH_ADDRESS.toString()] =
Expand Down Expand Up @@ -168,13 +216,35 @@ internal class DeviceParamNotAvailableFactoryImpl internal constructor(
params[DeviceParam.PARAM_SECURE_INSTALL_NON_MARKET_APPS.toString()] =
Reason.PERMISSION.toString()

params[DeviceParam.PARAM_MULTI_SIM_SUPPORTED.toString()] =
Reason.PERMISSION.toString()
params[DeviceParam.PARAM_6GHZ_BAND_SUPPORTED.toString()] =
Reason.PERMISSION.toString()
params[DeviceParam.PARAM_PASSPOINT_FQDN.toString()] =
Reason.PERMISSION.toString()
params[DeviceParam.PARAM_PASSPOINT_PROVIDER_FRIENDLY_NAME.toString()] =
Reason.PERMISSION.toString()
params[DeviceParam.PARAM_BONDED_DEVICES_ALIAS.toString()] =
Reason.PERMISSION.toString()

return params
}

private val unavailableParams: Map<String, String>
get() {
val params = HashMap<String, String>()

params[DeviceParam.PARAM_MANUFACTURER_CODE.toString()] =
Reason.UNAVAILABLE.toString()

return params
}

internal enum class Reason(private val code: String) {
MARKET_OR_REGION_RESTRICTION("RE01"),
PLATFORM_VERSION("RE02"),
PERMISSION("RE03");
PERMISSION("RE03"),
UNAVAILABLE("RE04");

override fun toString(): String {
return code
Expand Down
Loading
Loading