Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-16 14:44:02 +04:00
parent b9eb3a6ab4
commit b63f8be2ce
5 changed files with 384 additions and 22 deletions

View file

@ -0,0 +1,74 @@
# Running the App on a Device (ADB)
Practical notes for launching the app on a connected device for manual/on-device testing.
## Package & main activity
| | Value |
|---|---|
| Debug package (Google flavor) | `com.tangem.wallet.debug` |
| Main launcher activity | `com.tangem.tap.MainActivity` |
| Application class | `com.tangem.tap.TangemHiltApplication` |
## Launch the main screen (correct way)
```bash
adb shell am force-stop com.tangem.wallet.debug
adb shell am start -n com.tangem.wallet.debug/com.tangem.tap.MainActivity
```
Verify the real screen is in the foreground:
```bash
adb shell dumpsys activity activities | grep -i topResumedActivity | grep -i tangem
# expect: topResumedActivity=ActivityRecord{... com.tangem.wallet.debug/com.tangem.tap.MainActivity ...}
```
## Pitfall: do NOT launch via `monkey`
```bash
# ❌ ambiguous — may open the wrong screen
adb shell monkey -p com.tangem.wallet.debug -c android.intent.category.LAUNCHER 1
```
Debug builds bundle **LeakCanary**, which registers its own `LAUNCHER` activity ("Leaks").
The package therefore has **multiple** MAIN/LAUNCHER activities, so `monkey` (and
`cmd package resolve-activity`) resolves to Android's `ResolverActivity` / the wrong entry
and can open **LeakCanary instead of the app**. Always launch `MainActivity` explicitly with
`am start -n`.
List the launcher activities to confirm:
```bash
adb shell cmd package query-activities \
-a android.intent.action.MAIN -c android.intent.category.LAUNCHER \
| grep -iE "name=" | grep -i tangem
```
## Cold start vs warm start (matters for `Application.init()`)
`TangemApplication.init()` (Hilt setup, and one-time bootstrap such as backend-auth device
key generation + registration) runs **once per process**, on `Application.onCreate()`.
- **Warm start** (process already alive / resumed from background): `init()` does **not** re-run.
You will NOT see the startup log banner `APP STARTED` or any one-time bootstrap logs.
- **Cold start** (after `am force-stop`, or first launch): `init()` runs → look for the
`APP STARTED` banner in logcat (tag `TangemApplication`).
To reliably observe anything that happens in `init()`, always `force-stop` first, then launch.
> Note: `init()` runs on process start regardless of *which* activity brought the process up
> (even LeakCanary's). So process-scoped logs are still valid on a wrong-activity launch — but
> for observing actual app UI/flows, launch `MainActivity` explicitly.
## Build & install (Google debug)
```bash
./gradlew :app:assembleGoogleDebug
adb install -r app/build/outputs/apk/google/debug/app-google-debug.apk
```
## Product flavors
Flavors: `google`, `huawei` (dimension `services`); default dev flavor is `google`.
Build types: `debug`, `mocked`, `internal`, `external`, `release`.

View file

@ -5,15 +5,10 @@
"command": "npx",
"args": ["-y", "firebase-tools@latest", "mcp"]
},
"atlassian": {
"type": "stdio",
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
},
"notion": {
"type": "stdio",
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"]
}
}
}
}

View file

@ -12,6 +12,8 @@ plugins {
alias(deps.plugins.room) apply false
alias(deps.plugins.kotlin.compose.compiler) apply false
alias(deps.plugins.ksp) apply false
// Applied (not `apply false`) so the root project gets the aggregate `buildHealth` task.
alias(deps.plugins.dependency.analysis)
}
buildscript {
@ -21,6 +23,288 @@ buildscript {
}
}
// Dependency Analysis (DAGP) global configuration.
dependencyAnalysis {
structure {
// Treat co-versioned artifact splits as a single logical dependency, so the plugin doesn't
// advise "declare the transitive directly" when you depend on one part and use a sibling.
// e.g. `serialization-json` always brings `serialization-core` (@Serializable lives there).
bundle("kotlinx-serialization") {
includeDependency("org.jetbrains.kotlinx:kotlinx-serialization-core")
includeDependency("org.jetbrains.kotlinx:kotlinx-serialization-json")
}
// Hilt/Dagger is split across many artifacts (hilt-android pulls dagger, hilt-core,
// javax.inject; hilt-compiler pulls dagger-compiler). Declaring hilt-android + hilt-compiler
// is the single entry point — don't advise expanding each transitive separately.
bundle("hilt") {
primary("com.google.dagger:hilt-android")
includeGroup("com.google.dagger")
includeDependency("javax.inject:javax.inject")
}
// `junit-jupiter` is an aggregator over `-api`/`-params`/`-engine`. Tests import the `-api`
// package; declaring the `junit-jupiter` aggregator (deps.test.junit5) is the project standard.
bundle("junit5") {
primary("org.junit.jupiter:junit-jupiter")
includeGroup("org.junit.jupiter")
}
// MockK's DSL functions (every/coEvery/verify) live in `mockk-dsl`, pulled by `mockk`.
// Declaring `mockk` (deps.test.mockk) is enough.
bundle("mockk") {
primary("io.mockk:mockk")
includeGroup("io.mockk")
}
// DataStore is split into datastore-core / datastore-preferences-core / datastore-preferences,
// all co-versioned. Declaring the one you use covers the sibling packages.
bundle("androidx-datastore") {
includeGroup("androidx.datastore")
}
// web3j is split across core/abi/crypto/tuples (+ its rxjava reactive layer), all co-versioned.
// The generated Solidity contract wrappers expose these in their public API; declaring
// web3j-core (the entry point) covers the family. rxjava is web3j-only in this repo.
bundle("web3j") {
primary("org.web3j:core")
includeGroup("org.web3j")
includeDependency("io.reactivex.rxjava2:rxjava")
}
// Decompose's public API (ComponentContext etc.) extends Essenty owner interfaces, so the
// essenty artifacts are part of decompose's surface. Declaring `decompose` covers them.
bundle("decompose") {
primary("com.arkivanov.decompose:decompose")
includeGroup("com.arkivanov.decompose")
includeGroup("com.arkivanov.essenty")
}
// Jetpack Compose is split across ~15 co-versioned artifacts (ui, ui-graphics, ui-text,
// ui-unit, foundation, foundation-layout, animation, animation-core, runtime, runtime-saveable,
// material-ripple, …). Treat the whole `androidx.compose.*` family as one logical dependency.
bundle("compose") {
include("^androidx\\.compose\\..*")
}
// Co-versioned families split across many artifacts — declaring one covers the siblings.
bundle("androidx-lifecycle") {
primary("androidx.lifecycle:lifecycle-runtime-ktx")
includeGroup("androidx.lifecycle")
}
bundle("androidx-appcompat") {
primary("androidx.appcompat:appcompat")
includeGroup("androidx.appcompat")
}
bundle("androidx-paging") {
includeGroup("androidx.paging")
}
bundle("coil") {
includeGroup("io.coil-kt")
}
// Room is split into runtime/ktx/common/compiler/paging (+ its androidx.sqlite runtime).
// Declaring room-runtime covers the siblings.
bundle("room") {
primary("androidx.room:room-runtime")
includeGroup("androidx.room")
includeGroup("androidx.sqlite")
}
}
issues {
all {
// hilt-android / hilt-core get flagged as `api`: Kotlin `internal` @Inject classes (and
// public @Inject impls) compile to public bytecode carrying RUNTIME javax.inject annotations,
// so DAGP sees javax.inject in the ABI. The Hilt runtime is never genuinely public API — keep
// it `implementation` repo-wide.
onIncorrectConfiguration {
exclude(
"com.google.dagger:hilt-android",
"com.google.dagger:hilt-core",
)
}
// dagger-compiler is always pulled by hilt-compiler (declared via kapt(deps.hilt.kapt));
// no need to declare the annotation processor separately.
// :test:core is the documented single entry point for the unit-test stack — it re-exports
// junit5/mockk/truth/turbine/coroutines-test as `api`. Because those re-exports are excluded
// from the "declare directly" advice above, DAGP sees :test:core itself as unused. It isn't —
// keep declaring testImplementation(projects.test.core) and silence the false positive.
onUnusedDependencies {
exclude(":test:core")
}
onUsedTransitiveDependencies {
exclude("com.google.dagger:dagger-compiler")
// jsr305 is a ubiquitous CLASS-retention nullability-annotation transitive (pulled in
// by many libs); consumers tolerate its absence, it's never declared directly.
exclude("com.google.code.findbugs:jsr305")
// :test:core re-exports the unit-test stack as `api` and is the documented single
// entry point (testImplementation(projects.test.core)). Don't advise declaring its
// re-exports directly in every test module.
exclude(
"app.cash.turbine:turbine",
"com.google.truth:truth",
"io.mockk:mockk",
"org.jetbrains.kotlinx:kotlinx-coroutines-test",
"org.junit.jupiter:junit-jupiter",
)
// wire-runtime (protobuf) and jakarta.inject-api are deep transitives of the Visa /
// WalletConnect (reown) SDKs. A few generated/SDK-facing types reference them, but they're
// never declared directly — they always arrive with their owning SDK. Don't advise
// declaring them per-module.
exclude(
"com.squareup.wire:wire-runtime",
"jakarta.inject:jakarta.inject-api",
)
}
}
// In :libs:auth these are injected only into `internal` classes (DI modules / Default* impls).
// DAGP advises `api` because Kotlin `internal` compiles to public bytecode (and Hilt's
// generated `_Factory` classes expose the constructor types publicly) — a false positive, not
// a real ABI leak. Keep them `implementation`. Scoped to this module so genuine api advice in
// other modules still surfaces.
project(":libs:auth") {
onIncorrectConfiguration {
exclude(
"com.squareup.moshi:moshi",
":core:config-toggles",
":core:datasource",
":core:utils",
)
}
}
// Same Kotlin-`internal`-compiles-to-public false positive: these are used only inside
// `internal` classes / DI modules / method bodies (verified), not in the public ABI.
project(":libs:blockchain-sdk") {
onIncorrectConfiguration {
exclude(
"androidx.datastore:datastore-preferences",
"com.squareup.moshi:moshi",
":core:analytics",
":core:utils",
)
}
}
// config-toggles is used only inside the `internal` DefaultCardSdkFeatureToggles (the public
// CardSdkFeatureToggles interface is empty) — internal→public false positive.
project(":libs:tangem-sdk-api") {
onIncorrectConfiguration {
exclude(":core:config-toggles")
}
}
// web3j leaks into the ABI only via incidentally-public generated contract wrappers
// (ERC20/TangemPaymentAccount/…), not the module's intended public API (VisaContractInfoProvider
// / VisaContractInfo) — keep it `implementation` (see PR review), so silence the `api` advice.
project(":libs:visa") {
onIncorrectConfiguration {
exclude("org.web3j:core")
}
}
// datasource/utils are used only inside the `internal` DI module + Amplitude impl (the public
// ABTestsManager interface doesn't expose them) — internal→public false positive.
project(":core:ab-tests") {
onIncorrectConfiguration {
exclude(":core:datasource", ":core:utils")
}
}
// datasource is used only inside `internal` DI modules + LocalTogglesStorage (the public
// FeatureTogglesManager API doesn't expose it) — internal→public false positive.
project(":core:config-toggles") {
onIncorrectConfiguration {
exclude(":core:datasource")
}
}
// moshi-polymorphic-adapter is used only in @Provides bodies / as annotation args, never in a
// public signature — internal→public false positive, keep it `implementation`.
project(":core:datasource") {
onIncorrectConfiguration {
exclude("dev.onenowy.moshipolymorphicadapter:moshi-polymorphic-adapter")
}
}
// material is consumed only via resources (styles.xml inherits MaterialComponents themes), which
// DAGP can't see — it suggests runtimeOnly, but resource linking needs it on the compile
// classpath. Keep it `implementation`.
project(":core:ui") {
onRuntimeOnly {
exclude("com.google.android.material:material")
}
}
// core:error exposes UniversalError, a supertype of VisaActivationError that card consumes via
// domain:visa:models. The compiler needs the supertype on the classpath, but DAGP sees no direct
// reference and flags it unused — false positive, keep it `implementation`.
project(":domain:card") {
onUnusedDependencies {
exclude(":core:error")
}
}
// core:utils is deliberately re-exported as api from the ubiquitous domain:models module so the
// many consumers that use TangemLogger / utils through it keep compiling. Demoting it to
// implementation would cascade across the whole repo, so silence the incorrect-config advice.
project(":domain:models") {
onIncorrectConfiguration {
exclude(":core:utils")
}
}
// :domain:models is kept as api because domain:markets:models exposes CryptoCurrency.RawID (a
// domain:models type) in its public data classes (TokenMarket/RawMarketToken/TokenMarketParams).
// DAGP misses this nested-type ABI leak and advises implementation; that advice is a false negative.
project(":domain:markets:models") {
onIncorrectConfiguration {
exclude(":domain:models")
}
}
// :domain:core is kept as api in :domain:legacy and :domain:express because their public APIs
// (RampStateManager, ExpressServiceFetcher#getInitializationStatus) return Flow<Lce<...>> where Lce
// is a domain:core type. DAGP doesn't trace the generic type argument into the ABI and advises
// implementation; that advice is a false negative.
project(":domain:legacy") {
onIncorrectConfiguration {
exclude(":domain:core")
}
}
project(":domain:express") {
onIncorrectConfiguration {
exclude(":domain:core")
}
}
// :domain:core kept as api in :domain:onramp — public use cases (GetOnrampCurrenciesUseCase etc.)
// return EitherFlow<...> (a domain:core alias). DAGP doesn't trace the alias/generic into the ABI.
project(":domain:onramp") {
onIncorrectConfiguration {
exclude(":domain:core")
}
}
// arrow-core kept as api in :domain:onboarding — WasTwinsOnboardingShownUseCase#invoke returns
// Flow<Either<...>> (arrow.core.Either in the public ABI). DAGP advises implementation here (false
// negative on the generic type argument).
project(":domain:onboarding") {
onIncorrectConfiguration {
exclude("io.arrow-kt:arrow-core")
}
}
// :domain:core kept as api in :domain:wallets — GetUserWalletUseCase#invokeFlow returns
// EitherFlow<...> (a domain:core alias). DAGP false-negative on the alias/generic.
project(":domain:wallets") {
onIncorrectConfiguration {
exclude(":domain:core")
}
}
// :domain:models kept as api in :domain:yield-supply:models — YieldMarketToken (public data
// class) exposes SerializedBigDecimal (a domain:models type) in public fields. DAGP misses it.
project(":domain:yield-supply:models") {
onIncorrectConfiguration {
exclude(":domain:models")
}
}
// :common is deliberately re-exported as api from common:ui (a ubiquitous UI dependency) so the
// many feature modules that use TangemBlogUrlBuilder / common types through it keep compiling.
// Demoting it to implementation would cascade across the feature graph.
project(":common:ui") {
onIncorrectConfiguration {
exclude(":common")
}
}
// libs:crypto is deliberately re-exported as api from the ubiquitous :common module so the many
// feature modules that use BlockchainUtils / crypto helpers through it keep compiling. Demoting
// it to implementation would cascade across the feature graph.
project(":common") {
onIncorrectConfiguration {
exclude(":libs:crypto")
}
}
}
}
val clean by tasks.registering {
delete(rootProject.buildDir)
}
@ -37,6 +321,11 @@ val unitTest by tasks.registering {
}
subprojects {
// Dependency Analysis (DAGP) registers `projectHealth`/`reason` on each module. In 3.x the
// root application no longer auto-applies to subprojects, so apply it here. Reusing the plugin
// already resolved by the root `plugins {}` block keeps it in the same classloader as AGP/Kotlin.
apply(plugin = "com.autonomousapps.dependency-analysis")
// App module
plugins.withId("com.android.application") {
afterEvaluate {

View file

@ -1,8 +1,6 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -12,28 +10,35 @@ android {
dependencies {
implementation(projects.core.utils)
api(projects.domain.models)
api(projects.domain.appCurrency.models)
api(projects.domain.staking.models)
api(projects.libs.crypto)
// region Other libraries
implementation(deps.hilt.android)
// endregion
// region Firebase libraries
implementation(platform(deps.firebase.bom))
implementation(deps.firebase.analytics)
implementation(deps.firebase.crashlytics)
implementation(deps.firebase.messaging)
// end
// endregion
// region Core modules
implementation(projects.core.utils)
// endregion
implementation(deps.arrow.core)
// region Domain
api(projects.domain.models)
// endregion
testImplementation(projects.test.core)
// region Libs
// libs:crypto is intentionally re-exported (api): :common is ubiquitous and many feature modules
// use BlockchainUtils / crypto helpers through it. Demoting to implementation cascades across the
// feature graph, so keep it api despite DAGP's advice (suppressed below).
api(projects.libs.crypto)
// endregion
// region Tests
testImplementation(deps.test.junit5)
testImplementation(deps.test.truth)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// end
testImplementation(projects.test.core)
// endregion
}

View file

@ -264,7 +264,6 @@ coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" }
kotlin-coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutine" }
kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" }
kotlin-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" }
kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" }
desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }