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

Apollo: Release source code for 52.5 #160

Merged
merged 1 commit into from
Dec 4, 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
23 changes: 21 additions & 2 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,33 @@ follow [https://changelog.md/](https://changelog.md/) guidelines.

## [Unreleased]

## [52.4] - 2024-10-18
## [52.5] - 2024-12-02

### ADDED

- Reintroduced background notification processing reliability improvements

### FIXED

- Errors due to non ascii chars in http headers.
- Error regarding radioVersion in some devices.
- Error regarding getDataState in some devices.
- Error regarding TransactionTooLargeException in email report intent.

### CHANGED

- Made json serialization of background execution metrics more reliable.
- Added troubleshooting logs for decryption of operation metadata ciphertext.
- Trimmed stacktraces in error reports breadcrumbs and metadata

## [52.4] - 2024-11-18

### FIXED

- Removed some background notification processing reliability improvements that were causing
errors and crashes

## [52.3] - 2024-10-08
## [52.3] - 2024-11-08

### ADDED

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,55 @@ package io.muun.apollo.data.net.base.interceptor

import io.muun.apollo.data.net.base.BaseInterceptor
import io.muun.apollo.data.os.BackgroundExecutionMetricsProvider
import io.muun.apollo.domain.errors.data.MuunSerializationError
import io.muun.apollo.domain.model.user.User
import io.muun.apollo.domain.selector.UserSelector
import io.muun.common.net.HeaderUtils
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Request
import timber.log.Timber
import javax.inject.Inject

class BackgroundExecutionMetricsInterceptor @Inject constructor(
private val bemProvider: BackgroundExecutionMetricsProvider,
private val userSel: UserSelector,
) : BaseInterceptor() {

override fun processRequest(originalRequest: Request): Request {
return originalRequest.newBuilder()
.addHeader(
HeaderUtils.BACKGROUND_EXECUTION_METRICS,
Json.encodeToString(bemProvider.run())
).build()
.addBem(originalRequest)
.build()
}

private fun Request.Builder.addBem(originalRequest: Request): Request.Builder {
val encodeToJson = safelyEncodeJson(originalRequest)

return if (encodeToJson != null) {
try {
addHeader(HeaderUtils.BACKGROUND_EXECUTION_METRICS, encodeToJson)
} catch (e: Throwable) {
logError(originalRequest, e)
this
}
} else {
this
}
}

private fun safelyEncodeJson(originalRequest: Request): String? {
return try {
Json.encodeToString(bemProvider.run())
} catch (e: Throwable) {
logError(originalRequest, e)
null
}
}

private fun logError(originalRequest: Request, e: Throwable) {
val supportId = userSel.getOptional()
.flatMap { obj: User -> obj.supportId }
.orElse("Not logged in")
Timber.e(MuunSerializationError(supportId, originalRequest, e))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class BackgroundExecutionMetricsProvider @Inject constructor(
private val systemCapabilitiesProvider: SystemCapabilitiesProvider,
private val dateTimeZoneProvider: DateTimeZoneProvider,
private val localeInfoProvider: LocaleInfoProvider,
private val trafficStatsInfoProvider: TrafficStatsInfoProvider,
private val nfcProvider: NfcProvider,
) {

private val powerManager: PowerManager by lazy {
Expand Down Expand Up @@ -72,7 +74,24 @@ class BackgroundExecutionMetricsProvider @Inject constructor(
connectivityInfoProvider.proxySocks,
dateTimeZoneProvider.autoDateTime,
dateTimeZoneProvider.autoTimeZone,
dateTimeZoneProvider.timeZoneId
dateTimeZoneProvider.timeZoneId,
localeInfoProvider.dateFormat,
localeInfoProvider.regionCode,
dateTimeZoneProvider.calendarIdentifier,
trafficStatsInfoProvider.androidMobileRxTraffic,
telephonyInfoProvider.simOperatorId,
telephonyInfoProvider.simOperatorName,
telephonyInfoProvider.mobileNetworkId,
telephonyInfoProvider.mobileNetworkName,
telephonyInfoProvider.mobileRoaming,
telephonyInfoProvider.mobileDataStatus,
telephonyInfoProvider.mobileRadioType,
telephonyInfoProvider.mobileDataActivity,
connectivityInfoProvider.networkLink,
nfcProvider.hasNfcFeature(),
nfcProvider.hasNfcAdapter,
nfcProvider.isNfcEnabled,
nfcProvider.getNfcAntennaPosition().map { "${it.first};${it.second}" }.toTypedArray()
)

@Suppress("ArrayInDataClass")
Expand Down Expand Up @@ -114,7 +133,24 @@ class BackgroundExecutionMetricsProvider @Inject constructor(
private val proxySocks: String,
private val autoDateTime: Int,
private val autoTimeZone: Int,
private val timeZoneId: String
private val timeZoneId: String,
private val androidDateFormat: String,
private val regionCode: String,
private val androidCalendarIdentifier: String,
private val androidMobileRxTraffic: Long,
private val androidSimOperatorId: String,
private val androidSimOperatorName: String,
private val androidMobileOperatorId: String,
private val mobileOperatorName: String,
private val androidMobileRoaming: Boolean,
private val androidMobileDataStatus: Int,
private val androidMobileRadioType: Int,
private val androidMobileDataActivity: Int,
private val androidNetworkLink: ConnectivityInfoProvider.NetworkLink?,
private val androidHasNfcFeature: Boolean,
private val androidHasNfcAdapter: Boolean,
private val androidNfcEnabled: Boolean,
private val androidNfcAntennaPositions: Array<String>
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class BuildInfoProvider @Inject constructor() {
val time: Long,
val host: String,
val type: String,
val radioVersion: String,
val radioVersion: String?,
val securityPatch: String,
val baseOs: String,
)
Expand Down
46 changes: 46 additions & 0 deletions android/apollo/src/main/java/io/muun/apollo/data/os/NfcProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.muun.apollo.data.os

import android.content.Context
import android.content.pm.PackageManager
import android.nfc.NfcAdapter
import timber.log.Timber
import javax.inject.Inject

class NfcProvider @Inject constructor(private val context: Context) {

// We could use getSystemService(Context.NFC_SERVICE) as? NfcManager but it's just a wrapper
// of this.
private val nfcAdapter: NfcAdapter? = try {
NfcAdapter.getDefaultAdapter(context)
} catch (e: Throwable) {
null
}

fun hasNfcFeature(): Boolean {
val packageManager = context.packageManager
return packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
}

val hasNfcAdapter: Boolean
get() = nfcAdapter != null

val isNfcEnabled: Boolean
get() = nfcAdapter != null && nfcAdapter.isEnabled

fun getNfcAntennaPosition(): List<Pair<Float, Float>> {
val result = mutableListOf<Pair<Float, Float>>()

try {
if (OS.supportsAvailableNfcAntennas()) {
val antennas = nfcAdapter?.nfcAntennaInfo?.availableNfcAntennas.orEmpty()
for (antenna in antennas) {
result.add(Pair(antenna.locationX.toFloat(), antenna.locationY.toFloat()))
}
}
} catch (e: Exception) {
Timber.i("Error while reading NFC data from NFC compat device: ${e.message}")
}

return result
}
}
11 changes: 11 additions & 0 deletions android/apollo/src/main/java/io/muun/apollo/data/os/OS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ object OS {
fun supportsKeystoreExceptionPublicMethods(): Boolean =
isAndroidTiramisuOrNewer()

fun supportsAvailableNfcAntennas(): Boolean =
isAndroidUpsideDownCakeOrNewer()


// PRIVATE STUFF:

/**
Expand Down Expand Up @@ -313,4 +317,11 @@ object OS {
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
private fun isAndroidTiramisuOrNewer(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU

/**
* Whether this OS version is U-14-34 or newer.
*/
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun isAndroidUpsideDownCakeOrNewer(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.muun.common.Optional
import javax.inject.Inject

private const val UNKNOWN = "UNKNOWN"
private const val DATA_UNKNOWN = -1

// TODO open to make tests work with mockito. We should probably move to mockK
open class TelephonyInfoProvider @Inject constructor(context: Context) {
Expand All @@ -31,8 +32,17 @@ open class TelephonyInfoProvider @Inject constructor(context: Context) {
}

val dataState: String
get() {
return mapDataState(telephonyManager.dataState)
get() = try {
mapDataState(telephonyManager.dataState)
} catch (e: Exception) {
// 1. Docs mention UnsupportedOperationException If the device does not have
// PackageManager#FEATURE_TELEPHONY_DATA.
// 2. Undocumented, but we've observed SecurityException: Requires READ_PHONE_STATE in
// the wild, in some Samsung Android 5 devices. So, catching that as well.

// Using our own custom DATA_UNKNOWN instead of TelephonyManager.DATA_UNKNOWN as it was
// only added in API 29.
mapDataState(DATA_UNKNOWN)
}

val simRegion: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ class EmailReportManager @Inject constructor(
private val context: Context,
) {

fun buildEmailReport(report: CrashReport, presenterName: String): EmailReport {
fun buildAbridgedEmailReport(report: CrashReport, presenterName: String): EmailReport {
return buildEmailReport(report, presenterName, abridged = true)
}

fun buildEmailReport(
report: CrashReport,
presenterName: String,
abridged: Boolean = false,
): EmailReport {

val supportId = userSel.getOptional()
.flatMap { obj: User -> obj.supportId }
Expand All @@ -47,7 +55,7 @@ class EmailReportManager @Inject constructor(
.defaultRegion(telephonyInfoProvider.region.orElse("null"))
.rootHint(isRootedDeviceAction.actionNow())
.locale(context.locale())
.build()
.build(abridged)
}

private fun getFcmTokenHash() = try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import io.muun.apollo.domain.model.NightMode
import io.muun.apollo.domain.model.PaymentRequest
import io.muun.apollo.domain.model.report.CrashReport
import io.muun.common.model.OperationDirection
import java.util.*
import java.util.Locale

/**
* AnalyticsEvent list, shared with Falcon. Names are lower-cased into FBA event IDs, do not rename.
Expand Down Expand Up @@ -516,7 +516,8 @@ sealed class AnalyticsEvent(metadataKeyValues: List<Pair<String, Any>> = listOf(
"title" to report.getTrackingTitle(),
"tag" to report.tag,
"message" to report.message,
"error" to report.printError().replace("\n", " "),
// Trim error stacktraces to avoid problems (not ideal but should be more than enough)
"error" to report.printErrorForAnalytics(),
"metadata" to report.printMetadata()
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.muun.apollo.domain.errors.data

import io.muun.apollo.domain.errors.MuunError
import io.muun.common.exception.PotentialBug
import okhttp3.Request

class MuunSerializationError(
supportId: String,
originalRequest: Request,
cause: Throwable,
) : MuunError(cause), PotentialBug {

init {
metadata["supportId"] = supportId
metadata["request"] = originalRequest.url().uri().toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ public static byte[] decryptPayloadFromPeer(final PrivateKey userKey,
final NetworkParameters network) {

try {
Timber.i("Decrypting payload from peer: %s", payload);
// TODO: We should actually pass a peer key here
return toLibwalletModel(userKey, network)
.decrypterFrom(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ package io.muun.apollo.domain.model.report

import android.util.Log
import java.io.Serializable
import kotlin.math.min

/**
* This acts as a safe-guard when sending errors in our own email reports.
* Too many data can cause crashes (e.g TransactionTooLargeException in when trying use emailIntent
* for email reports).
*/
private const val STACK_TRACE_LIMIT_FOR_EMAIL_REPORTS = 10_000
private const val STACK_TRACE_LIMIT_FOR_ANALYTICS = 500

data class CrashReport(
val tag: String,
Expand All @@ -11,8 +20,8 @@ data class CrashReport(
val metadata: MutableMap<String, Serializable>,
) {

fun print() =
"Tag:$tag\nMessage:$message\nError:${printError()}\nMetadata:{\n\n${printMetadata()}}"
fun print(abridged: Boolean) =
"Message:$message\nError:${printError(abridged)}\nMetadata:{\n\n${printMetadata()}}"

fun printMetadata(): String {
val builder = StringBuilder()
Expand All @@ -22,8 +31,23 @@ data class CrashReport(
return builder.toString()
}

fun printError(): String =
Log.getStackTraceString(error)
private fun printError(abridged: Boolean = false): String {
val stackTraceString = Log.getStackTraceString(error)
return if (abridged) {
val stackTraceCapSizeInChars =
min(STACK_TRACE_LIMIT_FOR_EMAIL_REPORTS, stackTraceString.length)
return "${stackTraceString.substring(0, stackTraceCapSizeInChars)}\nEXCEEDED MAX LENGTH"
} else {
stackTraceString
}
}

fun printErrorForAnalytics(): String {
val stackTraceString = Log.getStackTraceString(error)
val stackTraceCapSizeInChars = min(STACK_TRACE_LIMIT_FOR_ANALYTICS, stackTraceString.length)
return stackTraceString.substring(0, stackTraceCapSizeInChars)
.replace("\n", " ")
}

fun getTrackingTitle(): String =
error.javaClass.simpleName + ":" + error.localizedMessage?.replace("\n", " ")
Expand Down
Loading