<NME> BreadApp.kt
<BEF> /**
 * BreadWallet
 *
 * Created by Drew Carlson <drew.carlson@breadwallet.com> on 9/23/19.
 * Copyright (c) 2019 breadwallet LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.breadwallet.app

import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.os.Build
import androidx.annotation.VisibleForTesting
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraXConfig
import androidx.core.content.edit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.breadwallet.BuildConfig
import com.breadwallet.breadbox.BdbAuthInterceptor
import com.breadwallet.breadbox.BreadBox
import com.breadwallet.breadbox.BreadBoxCloseWorker
import com.breadwallet.breadbox.CoreBreadBox
import com.breadwallet.corecrypto.CryptoApiProvider
import com.breadwallet.crypto.CryptoApi
import com.breadwallet.crypto.WalletManagerMode
import com.breadwallet.crypto.blockchaindb.BlockchainDb
import com.breadwallet.installHooks
import com.breadwallet.logger.logDebug
import com.breadwallet.logger.logError
import com.breadwallet.repository.ExperimentsRepository
import com.breadwallet.repository.ExperimentsRepositoryImpl
import com.breadwallet.repository.RatesRepository
import com.breadwallet.tools.crypto.Base32
import com.breadwallet.tools.crypto.CryptoHelper
import com.breadwallet.tools.manager.BRClipboardManager
import com.breadwallet.tools.manager.BRReportsManager
import com.breadwallet.tools.manager.BRSharedPrefs
import com.breadwallet.tools.manager.ConnectivityStateProvider
import com.breadwallet.tools.manager.InternetManager
import com.breadwallet.tools.manager.NetworkCallbacksConnectivityStateProvider
import com.breadwallet.tools.manager.RatesFetcher
import com.breadwallet.tools.security.BRKeyStore
import com.breadwallet.tools.security.BrdUserManager
import com.breadwallet.tools.security.BrdUserState
import com.breadwallet.tools.security.CryptoUserManager
import com.breadwallet.tools.services.BRDFirebaseMessagingService
import com.breadwallet.tools.util.BRConstants
import com.breadwallet.tools.util.EventUtils
import com.breadwallet.tools.util.ServerBundlesHelper
import com.breadwallet.tools.util.SupportManager
import com.breadwallet.tools.util.TokenUtil
import com.breadwallet.ui.uigift.GiftBackup
import com.breadwallet.ui.uigift.SharedPrefsGiftBackup
import com.breadwallet.util.AddressResolverServiceLocator
import com.breadwallet.util.CryptoUriParser
import com.breadwallet.util.FioService
import com.breadwallet.util.PayIdService
import com.breadwallet.util.errorHandler
import com.breadwallet.util.isEthereum
import com.breadwallet.util.usermetrics.UserMetricsUtil
import com.platform.APIClient
import com.platform.HTTPServer
import com.breadwallet.platform.interfaces.AccountMetaDataProvider
import com.platform.interfaces.KVStoreProvider
import com.platform.interfaces.MetaDataManager
import com.platform.interfaces.WalletProvider
import com.platform.sqlite.PlatformSqliteHelper
import com.platform.tools.KVStoreManager
import com.platform.tools.TokenHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.kodein.di.DKodein
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.direct
import org.kodein.di.erased.bind
import org.kodein.di.erased.instance
import org.kodein.di.erased.singleton
import java.io.File
import java.io.UnsupportedEncodingException
import java.util.Locale
import java.util.regex.Pattern

private const val LOCK_TIMEOUT = 180_000L // 3 minutes in milliseconds
private const val ENCRYPTED_PREFS_FILE = "crypto_shared_prefs"
private const val ENCRYPTED_GIFT_BACKUP_FILE = "gift_shared_prefs"
private const val WALLETKIT_DATA_DIR_NAME = "cryptocore"

@Suppress("TooManyFunctions")
class BreadApp : Application(), KodeinAware, CameraXConfig.Provider {

    companion object {
        init {
            CryptoApi.initialize(CryptoApiProvider.getInstance())
        }

        // The wallet ID is in the form "xxxx xxxx xxxx xxxx" where x is a lowercase letter or a number.
        private const val WALLET_ID_PATTERN = "^[a-z0-9 ]*$"
        private const val WALLET_ID_SEPARATOR = " "
        private const val NUMBER_OF_BYTES_FOR_SHA256_NEEDED = 10

        @SuppressLint("StaticFieldLeak")
        private lateinit var mInstance: BreadApp

        @SuppressLint("StaticFieldLeak")
        private var mCurrentActivity: Activity? = null

        /** [CoroutineScope] matching the lifetime of the application. */
        val applicationScope = CoroutineScope(
            SupervisorJob() + Dispatchers.Default + errorHandler("applicationScope")
        )

        private val startedScope = CoroutineScope(
            SupervisorJob() + Dispatchers.Default + errorHandler("startedScope")
        )

        fun getBreadBox(): BreadBox = mInstance.direct.instance()

        // TODO: Find better place/means for this
        fun getDefaultEnabledWallets() = when {
            BuildConfig.BITCOIN_TESTNET -> listOf(
                "bitcoin-testnet:__native__",
                "ethereum-ropsten:__native__",
                "ethereum-ropsten:0x558ec3152e2eb2174905cd19aea4e34a23de9ad6"
            )
            else -> listOf(
                "bitcoin-mainnet:__native__",
                "ethereum-mainnet:__native__",
                "ethereum-mainnet:0x558ec3152e2eb2174905cd19aea4e34a23de9ad6"
            )
        }

        fun getDefaultWalletModes() = when {
            BuildConfig.BITCOIN_TESTNET -> mapOf(
                "bitcoin-testnet:__native__" to WalletManagerMode.API_ONLY,
                "bitcoincash-testnet:__native__" to WalletManagerMode.API_ONLY
            )
            else -> mapOf(
                "bitcoin-mainnet:__native__" to WalletManagerMode.API_ONLY,
                "bitcoincash-mainnet:__native__" to WalletManagerMode.API_ONLY,
            )
        }

        /**
         * Initialize the wallet id (rewards id), and save it in the SharedPreferences.
         */
        private fun initializeWalletId() {
            applicationScope.launch(Dispatchers.Main) {
                val walletId = getBreadBox()
                    .wallets(false)
                    .mapNotNull { wallets ->
                        wallets.find { it.currency.code.isEthereum() }
                    }
                    .take(1)
                    .map { generateWalletId(it.target.toString()) }
                    .flowOn(Dispatchers.Default)
                    .first()
                if (walletId.isNullOrBlank() || !walletId.matches(WALLET_ID_PATTERN.toRegex())) {
                    val error = IllegalStateException("Generated corrupt walletId: $walletId")
                    BRReportsManager.reportBug(error)
                }
                BRSharedPrefs.putWalletRewardId(id = walletId ?: "")
            }
        }

        /**
         * Generates the wallet id (rewards id) based on the Ethereum address. The format of the id is
         * "xxxx xxxx xxxx xxxx", where x is a lowercase letter or a number.
         *
         * @return The wallet id.
         */
        // TODO: This entire operation should be moved into a separate class.
        @Synchronized
        @Suppress("ReturnCount")
        fun generateWalletId(address: String): String? {
            try {
                // Remove the first 2 characters i.e. 0x
                val rawAddress = address.drop(2)

                // Get the address bytes.
                val addressBytes = rawAddress.toByteArray()

                // Run SHA256 on the address bytes.
                val sha256Address = CryptoHelper.sha256(addressBytes) ?: byteArrayOf()
                if (sha256Address.isEmpty()) {
                    BRReportsManager.reportBug(IllegalAccessException("Failed to generate SHA256 hash."))
                    return null
                }

                // Get the first 10 bytes of the SHA256 hash.
                val firstTenBytes =
                    sha256Address.sliceArray(0 until NUMBER_OF_BYTES_FOR_SHA256_NEEDED)

                // Convert the first 10 bytes to a lower case string.
                val base32String = Base32.encode(firstTenBytes).toLowerCase(Locale.ROOT)

                // Insert a space every 4 chars to match the specified format.
                val builder = StringBuilder()
                val matcher = Pattern.compile(".{1,4}").matcher(base32String)
                var separator = ""
                while (matcher.find()) {
                    val piece = base32String.substring(matcher.start(), matcher.end())
                    builder.append(separator + piece)
                    separator = WALLET_ID_SEPARATOR
                }
                return builder.toString()
            } catch (e: UnsupportedEncodingException) {
                logError("Unable to get address bytes.", e)
                return null
            }
        }

        // TODO: Refactor so this does not store the current activity like this.
        @JvmStatic
        @Deprecated("")
        fun getBreadContext(): Context {
            var app: Context? = mCurrentActivity
            if (app == null) {
                app = mInstance
            }
            return app
        }

        // TODO: Refactor so this does not store the current activity like this.
        @JvmStatic
        fun setBreadContext(app: Activity?) {
            mCurrentActivity = app
        }

        /** Provides access to [DKodein]. Meant only for Java compatibility. **/
        @JvmStatic
        fun getKodeinInstance(): DKodein {
            return mInstance.direct
        }
    }

    override val kodein by Kodein.lazy {
        importOnce(androidXModule(this@BreadApp))

        bind<CryptoUriParser>() with singleton {
            CryptoUriParser(instance())
        }

        bind<APIClient>() with singleton {
            APIClient(this@BreadApp, direct.instance(), createHttpHeaders())
        }

        bind<BrdUserManager>() with singleton {
            CryptoUserManager(this@BreadApp, ::createCryptoEncryptedPrefs, instance())
        }

        bind<GiftBackup>() with singleton {
            SharedPrefsGiftBackup(::createGiftBackupEncryptedPrefs)
        }

        bind<KVStoreProvider>() with singleton {
            KVStoreManager(this@BreadApp)
        }

        val metaDataManager by lazy { MetaDataManager(direct.instance()) }

        bind<WalletProvider>() with singleton { metaDataManager }

        bind<AccountMetaDataProvider>() with singleton { metaDataManager }

        bind<OkHttpClient>() with singleton { OkHttpClient() }

        bind<BdbAuthInterceptor>() with singleton {
            val httpClient = instance<OkHttpClient>()
            BdbAuthInterceptor(httpClient, direct.instance())
        }

        bind<BlockchainDb>() with singleton {
            val httpClient = instance<OkHttpClient>()
            val authInterceptor = instance<BdbAuthInterceptor>()
            BlockchainDb(
                httpClient.newBuilder()
                    .addInterceptor(authInterceptor)
                    .build()
            )
        }

        bind<PayIdService>() with singleton {
            PayIdService(instance())
        }

        bind<FioService>() with singleton {
            FioService(instance())
        }

        bind<AddressResolverServiceLocator>() with singleton {
            AddressResolverServiceLocator(
                instance(),
                instance()
            )
        }

        bind<BreadBox>() with singleton {
            CoreBreadBox(
                File(filesDir, WALLETKIT_DATA_DIR_NAME),
                !BuildConfig.BITCOIN_TESTNET,
                instance(),
                instance(),
                instance()
            )
        }
            val authInterceptor = BdbAuthInterceptor(httpClient, direct.instance())
            BlockchainDb(
                httpClient.newBuilder()
                    .addInterceptor(authInterceptor)
                    .build()
            )
                instance(),
                instance(),
                this@BreadApp
            )
        }

        bind<ConversionTracker>() with singleton {
            ConversionTracker(instance())
        }

        bind<GiftTracker>() with singleton {
            GiftTracker(instance(), instance())
        }

        bind<ConnectivityStateProvider>() with singleton {
            val connectivityManager =
                this@BreadApp.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                NetworkCallbacksConnectivityStateProvider(connectivityManager)
            } else {
                InternetManager(connectivityManager,this@BreadApp)
            }
        }

        bind<SupportManager>() with singleton {
            SupportManager(
                this@BreadApp,
                instance(),
                instance()
            )
        }
    }

    private var accountLockJob: Job? = null

    private val apiClient by instance<APIClient>()
    private val giftTracker by instance<GiftTracker>()
    private val userManager by instance<BrdUserManager>()
    private val ratesFetcher by instance<RatesFetcher>()
    private val accountMetaData by instance<AccountMetaDataProvider>()
    private val conversionTracker by instance<ConversionTracker>()
    private val connectivityStateProvider by instance<ConnectivityStateProvider>()

    override fun onCreate() {
        super.onCreate()
        installHooks()
        mInstance = this

        BRKeyStore.provideContext(this)
        BRClipboardManager.provideContext(this)
        BRSharedPrefs.initialize(this, applicationScope)
        TokenHolder.provideContext(this)

        ProcessLifecycleOwner.get().lifecycle.addObserver(ApplicationLifecycleObserver())
        ApplicationLifecycleObserver.addApplicationLifecycleListener { event ->
            logDebug(event.name)
            when (event) {
                Lifecycle.Event.ON_START -> handleOnStart()
                Lifecycle.Event.ON_STOP -> handleOnStop()
                Lifecycle.Event.ON_DESTROY -> handleOnDestroy()
                else -> Unit
            }
        }

        applicationScope.launch {
            ServerBundlesHelper.extractBundlesIfNeeded(mInstance)
            TokenUtil.initialize(mInstance, false, !BuildConfig.BITCOIN_TESTNET)
        }

        // Start our local server as soon as the application instance is created, since we need to
        // display support WebViews during onboarding.
        HTTPServer.getInstance().startServer(this)
    }

    /**
     * Each time the app resumes, check to see if the device state is valid.
     * Even if the wallet is not initialized, we may need tell the user to enable the password.
     */
    private fun handleOnStart() {
        accountLockJob?.cancel()
        BreadBoxCloseWorker.cancelEnqueuedWork()
        val breadBox = getBreadBox()
        userManager
            .stateChanges()
            .distinctUntilChanged()
            .filterIsInstance<BrdUserState.Enabled>()
            .onEach {
                if (!userManager.isMigrationRequired()) {
                    startWithInitializedWallet(breadBox)
                }
            }
            .launchIn(startedScope)
    }

    private fun handleOnStop() {
        if (userManager.isInitialized()) {
            accountLockJob = applicationScope.launch {
                delay(LOCK_TIMEOUT)
                userManager.lock()
            }
            BreadBoxCloseWorker.enqueueWork()
            applicationScope.launch {
                EventUtils.saveEvents(this@BreadApp)
                EventUtils.pushToServer(this@BreadApp)
            }

        }
        logDebug("Shutting down HTTPServer.")
        HTTPServer.getInstance().stopServer()

        startedScope.coroutineContext.cancelChildren()
    }

    private fun handleOnDestroy() {
        if (HTTPServer.getInstance().isRunning) {
            logDebug("Shutting down HTTPServer.")
            HTTPServer.getInstance().stopServer()
        }

        connectivityStateProvider.close()
        getBreadBox().apply { if (isOpen) close() }
        applicationScope.cancel()
    }

    fun startWithInitializedWallet(breadBox: BreadBox, migrate: Boolean = false) {
        val context = mInstance.applicationContext
        incrementAppForegroundedCounter()

        if (!breadBox.isOpen) {
            val account = checkNotNull(userManager.getAccount()) {
                "Wallet is initialized but Account is null"
            }

            breadBox.open(account)
        }

        initializeWalletId()
        BRDFirebaseMessagingService.initialize(context)
        HTTPServer.getInstance().startServer(this)
        apiClient.updatePlatform(applicationScope)
        applicationScope.launch {
            UserMetricsUtil.makeUserMetricsRequest(context)
        }

        startedScope.launch {
            accountMetaData.recoverAll(migrate)
            giftTracker.checkUnclaimedGifts()
        }

        ratesFetcher.start(startedScope)
        
        conversionTracker.start(startedScope)
    }

    private fun incrementAppForegroundedCounter() {
        BRSharedPrefs.putInt(
            BRSharedPrefs.APP_FOREGROUNDED_COUNT,
            BRSharedPrefs.getInt(BRSharedPrefs.APP_FOREGROUNDED_COUNT, 0) + 1
        )
    }

    private fun createCryptoEncryptedPrefs(): SharedPreferences? = createEncryptedPrefs(ENCRYPTED_PREFS_FILE)
    private fun createGiftBackupEncryptedPrefs(): SharedPreferences? = createEncryptedPrefs(ENCRYPTED_GIFT_BACKUP_FILE)

    private fun createEncryptedPrefs(fileName: String): SharedPreferences? {
        val masterKeys = runCatching {
            MasterKey.Builder(this)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build()
        }.onFailure { e ->
            BRReportsManager.error("Failed to create Master Keys", e)
        }.getOrNull() ?: return null

        return runCatching {
            EncryptedSharedPreferences.create(
                this@BreadApp,
                fileName,
                masterKeys,
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
            )
        }.onFailure { e ->
            BRReportsManager.error("Failed to create Encrypted Shared Preferences", e)
        }.getOrNull()
    }

    override fun getCameraXConfig(): CameraXConfig {
        return Camera2Config.defaultConfig()
    }

    @VisibleForTesting
    fun clearApplicationData() {
        runCatching {
            startedScope.coroutineContext.cancelChildren()
            applicationScope.coroutineContext.cancelChildren()
            val breadBox = direct.instance<BreadBox>()
            if (breadBox.isOpen) {
                breadBox.close()
            }
            (userManager as CryptoUserManager).wipeAccount()

            File(filesDir, WALLETKIT_DATA_DIR_NAME).deleteRecursively()

            PlatformSqliteHelper.getInstance(this)
                .writableDatabase
                .delete(PlatformSqliteHelper.KV_STORE_TABLE_NAME, null, null)

            getSharedPreferences(BRSharedPrefs.PREFS_NAME, Context.MODE_PRIVATE).edit { clear() }
        }.onFailure { e ->
            logError("Failed to clear application data", e)
        }
    }

    fun createHttpHeaders(): Map<String, String> {
        // Split the default device user agent string by spaces and take the first string.
        // Example user agent string: "Dalvik/1.6.0 (Linux; U;Android 5.1; LG-F320SBuild/KOT49I.F320S22g) Android/9"
        // We only want: "Dalvik/1.6.0"
        val deviceUserAgent =
            (System.getProperty(APIClient.SYSTEM_PROPERTY_USER_AGENT) ?: "")
                .split("\\s".toRegex())
                .firstOrNull()

        // The BRD server expects the following user agent: appName/appVersion engine/engineVersion plaform/plaformVersion
        val brdUserAgent = "${APIClient.UA_APP_NAME}${BuildConfig.VERSION_CODE} $deviceUserAgent ${APIClient.UA_PLATFORM}${Build.VERSION.RELEASE}"

        return mapOf(
            APIClient.HEADER_IS_INTERNAL to if (BuildConfig.IS_INTERNAL_BUILD) BRConstants.TRUE else BRConstants.FALSE,
            APIClient.HEADER_TESTFLIGHT to if (BuildConfig.DEBUG) BRConstants.TRUE else BRConstants.FALSE,
            APIClient.HEADER_TESTNET to if (BuildConfig.BITCOIN_TESTNET) BRConstants.TRUE else BRConstants.FALSE,
            APIClient.HEADER_USER_AGENT to brdUserAgent
        )
    }
}

<MSG> Log url for large bdb response bodies

<DFF> @@ -348,6 +348,7 @@ class BreadApp : Application(), KodeinAware {
             val authInterceptor = BdbAuthInterceptor(httpClient, direct.instance())
             BlockchainDb(
                 httpClient.newBuilder()
+                    .addInterceptor(BdbLoggingInterceptor)
                     .addInterceptor(authInterceptor)
                     .build()
             )

{'commit': 'diff --git a/app/src/main/java/com/breadwallet/app/BdbLoggingInterceptor.kt b/app/src/main/java/com/breadwallet/app/BdbLoggingInterceptor.kt\nnew file mode 100644\nindex 000000000..4051d0a43\n--- /dev/null\n+++ b/app/src/main/java/com/breadwallet/app/BdbLoggingInterceptor.kt\n@@ -0,0 +1,45 @@\n+/**\n+ * BreadWallet\n+ *\n+ * Created by Drew Carlson <drew.carlson@breadwallet.com> on 3/26/20.\n+ * Copyright (c) 2020 breadwallet LLC\n+ *\n+ * Permission is hereby granted, free of charge, to any person obtaining a copy\n+ * of this software and associated documentation files (the "Software"), to deal\n+ * in the Software without restriction, including without limitation the rights\n+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+ * copies of the Software, and to permit persons to whom the Software is\n+ * furnished to do so, subject to the following conditions:\n+ *\n+ * The above copyright notice and this permission notice shall be included in\n+ * all copies or substantial portions of the Software.\n+ *\n+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n+ * THE SOFTWARE.\n+ */\n+package com.breadwallet.app\n+\n+import com.breadwallet.tools.manager.BRReportsManager\n+import okhttp3.Interceptor\n+import okhttp3.Response\n+\n+object BdbLoggingInterceptor : Interceptor {\n+    override fun intercept(chain: Interceptor.Chain): Response {\n+        val request = chain.request()\n+        val response = chain.proceed(request)\n+        response.body?.apply {\n+            val bytes = contentLength()\n+            if (bytes > 50_000_000L) {\n+                BRReportsManager.reportBug(\n+                    Exception("Large response body ($bytes) for \'${request.url}\'")\n+                )\n+            }\n+        }\n+        return response\n+    }\n+}\n\\ No newline at end of file\ndiff --git a/app/src/main/java/com/breadwallet/app/BreadApp.kt b/app/src/main/java/com/breadwallet/app/BreadApp.kt\nindex 5d7cefd90..68c18b3e1 100644\n--- a/app/src/main/java/com/breadwallet/app/BreadApp.kt\n+++ b/app/src/main/java/com/breadwallet/app/BreadApp.kt\n@@ -348,6 +348,7 @@ class BreadApp : Application(), KodeinAware {\n             val authInterceptor = BdbAuthInterceptor(httpClient, direct.instance())\n             BlockchainDb(\n                 httpClient.newBuilder()\n+                    .addInterceptor(BdbLoggingInterceptor)\n                     .addInterceptor(authInterceptor)\n                     .build()\n             )\n', 'commit_subject': 'Log url for large bdb response bodies', 'repo_name': 'breadwallet/breadwallet-android', 'committer': {'name': 'Drew Carlson', 'email': '98e34cb345eb537e565243a679c3e011566996b4@protonmail.com', 'time_sec': '1585261004', 'tz_offset': '-420', 'date': {'seconds': '1585261004'}}, 'commit_tree': 'e7d57eb0fdeb35fa32cb1562dfa7e0a503e7cdc5', 'commit_message': 'Log url for large bdb response bodies\n', 'language_name': 'HTML', 'license': 'mit', 'hunks': '@@ -348,6 +348,7 @@ class BreadApp : Application(), KodeinAware {\n             val authInterceptor = BdbAuthInterceptor(httpClient, direct.instance())\n             BlockchainDb(\n                 httpClient.newBuilder()\n+                    .addInterceptor(BdbLoggingInterceptor)\n                     .addInterceptor(authInterceptor)\n                     .build()\n             )\n', 'addition_count': 1, 'deletion_count': 0, 'src_file': 'a/app/src/main/java/com/breadwallet/app/BreadApp.kt', 'tgt_file': 'b/app/src/main/java/com/breadwallet/app/BreadApp.kt', 'file_extension': '.kt', 'before_file': '/**\n * BreadWallet\n *\n * Created by Drew Carlson <drew.carlson@breadwallet.com> on 9/23/19.\n * Copyright (c) 2019 breadwallet LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the "Software"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\npackage com.breadwallet.app\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Context\nimport android.content.SharedPreferences\nimport android.net.ConnectivityManager\nimport android.os.Build\nimport androidx.annotation.VisibleForTesting\nimport androidx.camera.camera2.Camera2Config\nimport androidx.camera.core.CameraXConfig\nimport androidx.core.content.edit\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.ProcessLifecycleOwner\nimport androidx.security.crypto.EncryptedSharedPreferences\nimport androidx.security.crypto.MasterKey\nimport com.breadwallet.BuildConfig\nimport com.breadwallet.breadbox.BdbAuthInterceptor\nimport com.breadwallet.breadbox.BreadBox\nimport com.breadwallet.breadbox.BreadBoxCloseWorker\nimport com.breadwallet.breadbox.CoreBreadBox\nimport com.breadwallet.corecrypto.CryptoApiProvider\nimport com.breadwallet.crypto.CryptoApi\nimport com.breadwallet.crypto.WalletManagerMode\nimport com.breadwallet.crypto.blockchaindb.BlockchainDb\nimport com.breadwallet.installHooks\nimport com.breadwallet.logger.logDebug\nimport com.breadwallet.logger.logError\nimport com.breadwallet.repository.ExperimentsRepository\nimport com.breadwallet.repository.ExperimentsRepositoryImpl\nimport com.breadwallet.repository.RatesRepository\nimport com.breadwallet.tools.crypto.Base32\nimport com.breadwallet.tools.crypto.CryptoHelper\nimport com.breadwallet.tools.manager.BRClipboardManager\nimport com.breadwallet.tools.manager.BRReportsManager\nimport com.breadwallet.tools.manager.BRSharedPrefs\nimport com.breadwallet.tools.manager.ConnectivityStateProvider\nimport com.breadwallet.tools.manager.InternetManager\nimport com.breadwallet.tools.manager.NetworkCallbacksConnectivityStateProvider\nimport com.breadwallet.tools.manager.RatesFetcher\nimport com.breadwallet.tools.security.BRKeyStore\nimport com.breadwallet.tools.security.BrdUserManager\nimport com.breadwallet.tools.security.BrdUserState\nimport com.breadwallet.tools.security.CryptoUserManager\nimport com.breadwallet.tools.services.BRDFirebaseMessagingService\nimport com.breadwallet.tools.util.BRConstants\nimport com.breadwallet.tools.util.EventUtils\nimport com.breadwallet.tools.util.ServerBundlesHelper\nimport com.breadwallet.tools.util.SupportManager\nimport com.breadwallet.tools.util.TokenUtil\nimport com.breadwallet.ui.uigift.GiftBackup\nimport com.breadwallet.ui.uigift.SharedPrefsGiftBackup\nimport com.breadwallet.util.AddressResolverServiceLocator\nimport com.breadwallet.util.CryptoUriParser\nimport com.breadwallet.util.FioService\nimport com.breadwallet.util.PayIdService\nimport com.breadwallet.util.errorHandler\nimport com.breadwallet.util.isEthereum\nimport com.breadwallet.util.usermetrics.UserMetricsUtil\nimport com.platform.APIClient\nimport com.platform.HTTPServer\nimport com.breadwallet.platform.interfaces.AccountMetaDataProvider\nimport com.platform.interfaces.KVStoreProvider\nimport com.platform.interfaces.MetaDataManager\nimport com.platform.interfaces.WalletProvider\nimport com.platform.sqlite.PlatformSqliteHelper\nimport com.platform.tools.KVStoreManager\nimport com.platform.tools.TokenHolder\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.cancelChildren\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.filterIsInstance\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.flow.launchIn\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.take\nimport kotlinx.coroutines.launch\nimport okhttp3.OkHttpClient\nimport org.kodein.di.DKodein\nimport org.kodein.di.Kodein\nimport org.kodein.di.KodeinAware\nimport org.kodein.di.android.x.androidXModule\nimport org.kodein.di.direct\nimport org.kodein.di.erased.bind\nimport org.kodein.di.erased.instance\nimport org.kodein.di.erased.singleton\nimport java.io.File\nimport java.io.UnsupportedEncodingException\nimport java.util.Locale\nimport java.util.regex.Pattern\n\nprivate const val LOCK_TIMEOUT = 180_000L // 3 minutes in milliseconds\nprivate const val ENCRYPTED_PREFS_FILE = "crypto_shared_prefs"\nprivate const val ENCRYPTED_GIFT_BACKUP_FILE = "gift_shared_prefs"\nprivate const val WALLETKIT_DATA_DIR_NAME = "cryptocore"\n\n@Suppress("TooManyFunctions")\nclass BreadApp : Application(), KodeinAware, CameraXConfig.Provider {\n\n    companion object {\n        init {\n            CryptoApi.initialize(CryptoApiProvider.getInstance())\n        }\n\n        // The wallet ID is in the form "xxxx xxxx xxxx xxxx" where x is a lowercase letter or a number.\n        private const val WALLET_ID_PATTERN = "^[a-z0-9 ]*$"\n        private const val WALLET_ID_SEPARATOR = " "\n        private const val NUMBER_OF_BYTES_FOR_SHA256_NEEDED = 10\n\n        @SuppressLint("StaticFieldLeak")\n        private lateinit var mInstance: BreadApp\n\n        @SuppressLint("StaticFieldLeak")\n        private var mCurrentActivity: Activity? = null\n\n        /** [CoroutineScope] matching the lifetime of the application. */\n        val applicationScope = CoroutineScope(\n            SupervisorJob() + Dispatchers.Default + errorHandler("applicationScope")\n        )\n\n        private val startedScope = CoroutineScope(\n            SupervisorJob() + Dispatchers.Default + errorHandler("startedScope")\n        )\n\n        fun getBreadBox(): BreadBox = mInstance.direct.instance()\n\n        // TODO: Find better place/means for this\n        fun getDefaultEnabledWallets() = when {\n            BuildConfig.BITCOIN_TESTNET -> listOf(\n                "bitcoin-testnet:__native__",\n                "ethereum-ropsten:__native__",\n                "ethereum-ropsten:0x558ec3152e2eb2174905cd19aea4e34a23de9ad6"\n            )\n            else -> listOf(\n                "bitcoin-mainnet:__native__",\n                "ethereum-mainnet:__native__",\n                "ethereum-mainnet:0x558ec3152e2eb2174905cd19aea4e34a23de9ad6"\n            )\n        }\n\n        fun getDefaultWalletModes() = when {\n            BuildConfig.BITCOIN_TESTNET -> mapOf(\n                "bitcoin-testnet:__native__" to WalletManagerMode.API_ONLY,\n                "bitcoincash-testnet:__native__" to WalletManagerMode.API_ONLY\n            )\n            else -> mapOf(\n                "bitcoin-mainnet:__native__" to WalletManagerMode.API_ONLY,\n                "bitcoincash-mainnet:__native__" to WalletManagerMode.API_ONLY,\n            )\n        }\n\n        /**\n         * Initialize the wallet id (rewards id), and save it in the SharedPreferences.\n         */\n        private fun initializeWalletId() {\n            applicationScope.launch(Dispatchers.Main) {\n                val walletId = getBreadBox()\n                    .wallets(false)\n                    .mapNotNull { wallets ->\n                        wallets.find { it.currency.code.isEthereum() }\n                    }\n                    .take(1)\n                    .map { generateWalletId(it.target.toString()) }\n                    .flowOn(Dispatchers.Default)\n                    .first()\n                if (walletId.isNullOrBlank() || !walletId.matches(WALLET_ID_PATTERN.toRegex())) {\n                    val error = IllegalStateException("Generated corrupt walletId: $walletId")\n                    BRReportsManager.reportBug(error)\n                }\n                BRSharedPrefs.putWalletRewardId(id = walletId ?: "")\n            }\n        }\n\n        /**\n         * Generates the wallet id (rewards id) based on the Ethereum address. The format of the id is\n         * "xxxx xxxx xxxx xxxx", where x is a lowercase letter or a number.\n         *\n         * @return The wallet id.\n         */\n        // TODO: This entire operation should be moved into a separate class.\n        @Synchronized\n        @Suppress("ReturnCount")\n        fun generateWalletId(address: String): String? {\n            try {\n                // Remove the first 2 characters i.e. 0x\n                val rawAddress = address.drop(2)\n\n                // Get the address bytes.\n                val addressBytes = rawAddress.toByteArray()\n\n                // Run SHA256 on the address bytes.\n                val sha256Address = CryptoHelper.sha256(addressBytes) ?: byteArrayOf()\n                if (sha256Address.isEmpty()) {\n                    BRReportsManager.reportBug(IllegalAccessException("Failed to generate SHA256 hash."))\n                    return null\n                }\n\n                // Get the first 10 bytes of the SHA256 hash.\n                val firstTenBytes =\n                    sha256Address.sliceArray(0 until NUMBER_OF_BYTES_FOR_SHA256_NEEDED)\n\n                // Convert the first 10 bytes to a lower case string.\n                val base32String = Base32.encode(firstTenBytes).toLowerCase(Locale.ROOT)\n\n                // Insert a space every 4 chars to match the specified format.\n                val builder = StringBuilder()\n                val matcher = Pattern.compile(".{1,4}").matcher(base32String)\n                var separator = ""\n                while (matcher.find()) {\n                    val piece = base32String.substring(matcher.start(), matcher.end())\n                    builder.append(separator + piece)\n                    separator = WALLET_ID_SEPARATOR\n                }\n                return builder.toString()\n            } catch (e: UnsupportedEncodingException) {\n                logError("Unable to get address bytes.", e)\n                return null\n            }\n        }\n\n        // TODO: Refactor so this does not store the current activity like this.\n        @JvmStatic\n        @Deprecated("")\n        fun getBreadContext(): Context {\n            var app: Context? = mCurrentActivity\n            if (app == null) {\n                app = mInstance\n            }\n            return app\n        }\n\n        // TODO: Refactor so this does not store the current activity like this.\n        @JvmStatic\n        fun setBreadContext(app: Activity?) {\n            mCurrentActivity = app\n        }\n\n        /** Provides access to [DKodein]. Meant only for Java compatibility. **/\n        @JvmStatic\n        fun getKodeinInstance(): DKodein {\n            return mInstance.direct\n        }\n    }\n\n    override val kodein by Kodein.lazy {\n        importOnce(androidXModule(this@BreadApp))\n\n        bind<CryptoUriParser>() with singleton {\n            CryptoUriParser(instance())\n        }\n\n        bind<APIClient>() with singleton {\n            APIClient(this@BreadApp, direct.instance(), createHttpHeaders())\n        }\n\n        bind<BrdUserManager>() with singleton {\n            CryptoUserManager(this@BreadApp, ::createCryptoEncryptedPrefs, instance())\n        }\n\n        bind<GiftBackup>() with singleton {\n            SharedPrefsGiftBackup(::createGiftBackupEncryptedPrefs)\n        }\n\n        bind<KVStoreProvider>() with singleton {\n            KVStoreManager(this@BreadApp)\n        }\n\n        val metaDataManager by lazy { MetaDataManager(direct.instance()) }\n\n        bind<WalletProvider>() with singleton { metaDataManager }\n\n        bind<AccountMetaDataProvider>() with singleton { metaDataManager }\n\n        bind<OkHttpClient>() with singleton { OkHttpClient() }\n\n        bind<BdbAuthInterceptor>() with singleton {\n            val httpClient = instance<OkHttpClient>()\n            BdbAuthInterceptor(httpClient, direct.instance())\n        }\n\n        bind<BlockchainDb>() with singleton {\n            val httpClient = instance<OkHttpClient>()\n            val authInterceptor = instance<BdbAuthInterceptor>()\n            BlockchainDb(\n                httpClient.newBuilder()\n                    .addInterceptor(authInterceptor)\n                    .build()\n            )\n        }\n\n        bind<PayIdService>() with singleton {\n            PayIdService(instance())\n        }\n\n        bind<FioService>() with singleton {\n            FioService(instance())\n        }\n\n        bind<AddressResolverServiceLocator>() with singleton {\n            AddressResolverServiceLocator(\n                instance(),\n                instance()\n            )\n        }\n\n        bind<BreadBox>() with singleton {\n            CoreBreadBox(\n                File(filesDir, WALLETKIT_DATA_DIR_NAME),\n                !BuildConfig.BITCOIN_TESTNET,\n                instance(),\n                instance(),\n                instance()\n            )\n        }\n            val authInterceptor = BdbAuthInterceptor(httpClient, direct.instance())\n            BlockchainDb(\n                httpClient.newBuilder()\n                    .addInterceptor(authInterceptor)\n                    .build()\n            )\n                instance(),\n                instance(),\n                this@BreadApp\n            )\n        }\n\n        bind<ConversionTracker>() with singleton {\n            ConversionTracker(instance())\n        }\n\n        bind<GiftTracker>() with singleton {\n            GiftTracker(instance(), instance())\n        }\n\n        bind<ConnectivityStateProvider>() with singleton {\n            val connectivityManager =\n                this@BreadApp.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n                NetworkCallbacksConnectivityStateProvider(connectivityManager)\n            } else {\n                InternetManager(connectivityManager,this@BreadApp)\n            }\n        }\n\n        bind<SupportManager>() with singleton {\n            SupportManager(\n                this@BreadApp,\n                instance(),\n                instance()\n            )\n        }\n    }\n\n    private var accountLockJob: Job? = null\n\n    private val apiClient by instance<APIClient>()\n    private val giftTracker by instance<GiftTracker>()\n    private val userManager by instance<BrdUserManager>()\n    private val ratesFetcher by instance<RatesFetcher>()\n    private val accountMetaData by instance<AccountMetaDataProvider>()\n    private val conversionTracker by instance<ConversionTracker>()\n    private val connectivityStateProvider by instance<ConnectivityStateProvider>()\n\n    override fun onCreate() {\n        super.onCreate()\n        installHooks()\n        mInstance = this\n\n        BRKeyStore.provideContext(this)\n        BRClipboardManager.provideContext(this)\n        BRSharedPrefs.initialize(this, applicationScope)\n        TokenHolder.provideContext(this)\n\n        ProcessLifecycleOwner.get().lifecycle.addObserver(ApplicationLifecycleObserver())\n        ApplicationLifecycleObserver.addApplicationLifecycleListener { event ->\n            logDebug(event.name)\n            when (event) {\n                Lifecycle.Event.ON_START -> handleOnStart()\n                Lifecycle.Event.ON_STOP -> handleOnStop()\n                Lifecycle.Event.ON_DESTROY -> handleOnDestroy()\n                else -> Unit\n            }\n        }\n\n        applicationScope.launch {\n            ServerBundlesHelper.extractBundlesIfNeeded(mInstance)\n            TokenUtil.initialize(mInstance, false, !BuildConfig.BITCOIN_TESTNET)\n        }\n\n        // Start our local server as soon as the application instance is created, since we need to\n        // display support WebViews during onboarding.\n        HTTPServer.getInstance().startServer(this)\n    }\n\n    /**\n     * Each time the app resumes, check to see if the device state is valid.\n     * Even if the wallet is not initialized, we may need tell the user to enable the password.\n     */\n    private fun handleOnStart() {\n        accountLockJob?.cancel()\n        BreadBoxCloseWorker.cancelEnqueuedWork()\n        val breadBox = getBreadBox()\n        userManager\n            .stateChanges()\n            .distinctUntilChanged()\n            .filterIsInstance<BrdUserState.Enabled>()\n            .onEach {\n                if (!userManager.isMigrationRequired()) {\n                    startWithInitializedWallet(breadBox)\n                }\n            }\n            .launchIn(startedScope)\n    }\n\n    private fun handleOnStop() {\n        if (userManager.isInitialized()) {\n            accountLockJob = applicationScope.launch {\n                delay(LOCK_TIMEOUT)\n                userManager.lock()\n            }\n            BreadBoxCloseWorker.enqueueWork()\n            applicationScope.launch {\n                EventUtils.saveEvents(this@BreadApp)\n                EventUtils.pushToServer(this@BreadApp)\n            }\n\n        }\n        logDebug("Shutting down HTTPServer.")\n        HTTPServer.getInstance().stopServer()\n\n        startedScope.coroutineContext.cancelChildren()\n    }\n\n    private fun handleOnDestroy() {\n        if (HTTPServer.getInstance().isRunning) {\n            logDebug("Shutting down HTTPServer.")\n            HTTPServer.getInstance().stopServer()\n        }\n\n        connectivityStateProvider.close()\n        getBreadBox().apply { if (isOpen) close() }\n        applicationScope.cancel()\n    }\n\n    fun startWithInitializedWallet(breadBox: BreadBox, migrate: Boolean = false) {\n        val context = mInstance.applicationContext\n        incrementAppForegroundedCounter()\n\n        if (!breadBox.isOpen) {\n            val account = checkNotNull(userManager.getAccount()) {\n                "Wallet is initialized but Account is null"\n            }\n\n            breadBox.open(account)\n        }\n\n        initializeWalletId()\n        BRDFirebaseMessagingService.initialize(context)\n        HTTPServer.getInstance().startServer(this)\n        apiClient.updatePlatform(applicationScope)\n        applicationScope.launch {\n            UserMetricsUtil.makeUserMetricsRequest(context)\n        }\n\n        startedScope.launch {\n            accountMetaData.recoverAll(migrate)\n            giftTracker.checkUnclaimedGifts()\n        }\n\n        ratesFetcher.start(startedScope)\n        \n        conversionTracker.start(startedScope)\n    }\n\n    private fun incrementAppForegroundedCounter() {\n        BRSharedPrefs.putInt(\n            BRSharedPrefs.APP_FOREGROUNDED_COUNT,\n            BRSharedPrefs.getInt(BRSharedPrefs.APP_FOREGROUNDED_COUNT, 0) + 1\n        )\n    }\n\n    private fun createCryptoEncryptedPrefs(): SharedPreferences? = createEncryptedPrefs(ENCRYPTED_PREFS_FILE)\n    private fun createGiftBackupEncryptedPrefs(): SharedPreferences? = createEncryptedPrefs(ENCRYPTED_GIFT_BACKUP_FILE)\n\n    private fun createEncryptedPrefs(fileName: String): SharedPreferences? {\n        val masterKeys = runCatching {\n            MasterKey.Builder(this)\n                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n                .build()\n        }.onFailure { e ->\n            BRReportsManager.error("Failed to create Master Keys", e)\n        }.getOrNull() ?: return null\n\n        return runCatching {\n            EncryptedSharedPreferences.create(\n                this@BreadApp,\n                fileName,\n                masterKeys,\n                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n            )\n        }.onFailure { e ->\n            BRReportsManager.error("Failed to create Encrypted Shared Preferences", e)\n        }.getOrNull()\n    }\n\n    override fun getCameraXConfig(): CameraXConfig {\n        return Camera2Config.defaultConfig()\n    }\n\n    @VisibleForTesting\n    fun clearApplicationData() {\n        runCatching {\n            startedScope.coroutineContext.cancelChildren()\n            applicationScope.coroutineContext.cancelChildren()\n            val breadBox = direct.instance<BreadBox>()\n            if (breadBox.isOpen) {\n                breadBox.close()\n            }\n            (userManager as CryptoUserManager).wipeAccount()\n\n            File(filesDir, WALLETKIT_DATA_DIR_NAME).deleteRecursively()\n\n            PlatformSqliteHelper.getInstance(this)\n                .writableDatabase\n                .delete(PlatformSqliteHelper.KV_STORE_TABLE_NAME, null, null)\n\n            getSharedPreferences(BRSharedPrefs.PREFS_NAME, Context.MODE_PRIVATE).edit { clear() }\n        }.onFailure { e ->\n            logError("Failed to clear application data", e)\n        }\n    }\n\n    fun createHttpHeaders(): Map<String, String> {\n        // Split the default device user agent string by spaces and take the first string.\n        // Example user agent string: "Dalvik/1.6.0 (Linux; U;Android 5.1; LG-F320SBuild/KOT49I.F320S22g) Android/9"\n        // We only want: "Dalvik/1.6.0"\n        val deviceUserAgent =\n            (System.getProperty(APIClient.SYSTEM_PROPERTY_USER_AGENT) ?: "")\n                .split("\\\\s".toRegex())\n                .firstOrNull()\n\n        // The BRD server expects the following user agent: appName/appVersion engine/engineVersion plaform/plaformVersion\n        val brdUserAgent = "${APIClient.UA_APP_NAME}${BuildConfig.VERSION_CODE} $deviceUserAgent ${APIClient.UA_PLATFORM}${Build.VERSION.RELEASE}"\n\n        return mapOf(\n            APIClient.HEADER_IS_INTERNAL to if (BuildConfig.IS_INTERNAL_BUILD) BRConstants.TRUE else BRConstants.FALSE,\n            APIClient.HEADER_TESTFLIGHT to if (BuildConfig.DEBUG) BRConstants.TRUE else BRConstants.FALSE,\n            APIClient.HEADER_TESTNET to if (BuildConfig.BITCOIN_TESTNET) BRConstants.TRUE else BRConstants.FALSE,\n            APIClient.HEADER_USER_AGENT to brdUserAgent\n        )\n    }\n}\n', 'message': 'Log url for large bdb response bodies\n', 'author_name': 'Drew Carlson', 'lang': 'kt'}