Skip to content
Draft
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ agent.md
.codex/
.github/copilot-instructions.md
.vscode/
/screenshots/
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ dependencies {
}
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:$desugarJdkLibsVersion")
implementation(platform("com.google.firebase:firebase-bom:$firebaseBomVersion"))
implementation("com.google.firebase:firebase-perf")
releaseImplementation("com.google.firebase:firebase-perf")
implementation(fileTree(mapOf("include" to listOf("*.aar"), "dir" to "libs")))
implementation("androidx.fragment:fragment-ktx:$fragmentVersion")
implementation("androidx.activity:activity-ktx:$activity_version")
Expand Down Expand Up @@ -439,7 +439,7 @@ dependencies {

implementation("com.google.protobuf:protobuf-javalite") {
version {
strictly("3.11.0")
strictly("3.25.5")
}
}

Expand Down Expand Up @@ -556,6 +556,7 @@ dependencies {
}
androidTestImplementation("androidx.test.espresso:espresso-idling-resource:$espressoVersion")
androidTestImplementation("androidx.test.ext:junit:$androidxJunitVersion")
androidTestImplementation("org.hamcrest:hamcrest:2.2")
androidTestImplementation("androidx.fragment:fragment-testing:$fragmentVersion")
androidTestImplementation("androidx.navigation:navigation-testing:$navigationVersion")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ class CustomTestRunner : AndroidJUnitRunner() {
name: String?,
context: Context?,
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
return super.newApplication(cl, MixinTestApplication_Application::class.java.name, context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package one.mixin.android

import dagger.hilt.android.testing.CustomTestApplication

@CustomTestApplication(MixinApplication::class)
interface MixinTestApplication
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package one.mixin.android.ui.home.web3.trade.perps

import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.view.PixelCopy
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import one.mixin.android.HiltTestActivity
import one.mixin.android.api.response.perps.PerpsMarket
import one.mixin.android.compose.theme.MixinAppTheme
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class TopMoversScreenshotTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()

@Test
fun captureTopMoversCard_Light() {
captureScreenshot(darkTheme = false, fileName = "top_movers_card_light.png")
}

@Test
fun captureTopMoversCard_Dark() {
captureScreenshot(darkTheme = true, fileName = "top_movers_card_dark.png")
}

private fun captureScreenshot(darkTheme: Boolean, fileName: String) {
hiltRule.inject()

composeTestRule.setContent {
MixinAppTheme(darkTheme = darkTheme) {
Surface(
color = MixinAppTheme.colors.background,
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
TopMoversCard(
markets = topMoverMarkets,
quoteColorReversed = false,
onViewAllClick = {},
onMarketItemClick = {},
)
}
}
}
}

composeTestRule.onNodeWithText("HYPE").assertExists()

Thread.sleep(5000)

val activity = composeTestRule.activity
val window = activity.window
val view = window.decorView
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val latch = CountDownLatch(1)

PixelCopy.request(window, bitmap, { copyResult ->
if (copyResult == PixelCopy.SUCCESS) {
latch.countDown()
}
}, Handler(Looper.getMainLooper()))

latch.await(5, TimeUnit.SECONDS)

val outputDir = File("/sdcard/Android/media/one.mixin.messenger/additional_test_output")
if (!outputDir.exists()) outputDir.mkdirs()
val screenshot = File(outputDir, fileName)
FileOutputStream(screenshot).use { output ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
}
}

private val topMoverMarkets = listOf(
topMoverMarket("04315ccb-211c-3a12-b28f-60fec2ea69e8", "HYPE", "0.0268", 100, "https://coin-images.coingecko.com/coins/images/50882/large/hyperliquid.jpg?1729431300"),
topMoverMarket("411aae6f-2596-3668-9fca-85f1c4dcd3c6", "TON", "0.0245", 100, "https://images.mixin.one/Qh7MjeINQ6ad68E0FI4iS7bbLGEuF7CZJlTkW1kSAiq8EaFngIZ1tDG0CRHz_hjz8gsiTmHKcdu_0UE1ugmUiHzNwJRm9fjoqJcb=s128"),
topMoverMarket("2c03fc3c-f7c9-39bd-8cdb-ba8a52476dc1", "ALGO", "0.0240", 100, "https://images.mixin.one/-oE4Jsi3aMIkxUPUsvDozyL8D0ccmPkggIdIDu1z8THDQyJcCIsbNwC4amFBiRlkQiLNNjiuMBsNw8sAnehTuI0=s128"),
topMoverMarket("c0349d7b-1b40-3fb7-804a-475abf4aadb7", "BERA", "0.0195", 100, "https://coin-images.coingecko.com/coins/images/25235/large/BERA.png?1738822008"),
topMoverMarket("9aa033e3-5ee4-320c-aa7f-b55b6ccd3a4b", "UNI", "-0.0229", 100, "https://images.mixin.one/Ekf9UzoHhRRfcDLchjfVrRPYZ_71jQt306WcvgwZWwEM2BIGlHcUm_sK3Nw_mjARPwIvNB9xAzAEWJyW86pVuarPu8O5YZ0WwqTo=s128"),
topMoverMarket("32fbceaf-0be1-3039-b721-bc6f638c7f92", "AXS", "-0.0170", 100, "https://images.mixin.one/WiJjvgFGEAHd0Fg8Z5m0eKNpO1f5Frevp2Yyu6KT09zuOZ7-t7tEcfrKVQYPcoJlAxpruILNBD5A05lvXarINxkgRPFGWWwj95Gg=s128"),
topMoverMarket("98bbcbfb-a040-33a2-911f-a7729346b00b", "SUI", "-0.0162", 100, "https://coin-images.mixinpay.com/fe432916-83f8-4d9f-3170-acfa2d1cad00/public"),
topMoverMarket("ced36291-082c-317d-b5b9-4be7e4965dcc", "CRV", "-0.0151", 100, "https://images.mixin.one/ZeFl04CufYhd1_DXRqnqe9xxLEGqVHDCGpDsgfHSnfNH9gYpcKwl2ELYPhLceSjDLO-iglj3pKFpiPN1y2c8QMm0YaURfXvsVH26gQ=s128"),
)

private fun topMoverMarket(
id: String,
symbol: String,
change: String,
leverage: Int,
iconUrl: String,
) = PerpsMarket(
marketId = id,
displaySymbol = "$symbol-PERP",
tokenSymbol = symbol,
quoteSymbol = "USDT",
markPrice = "100.00",
leverage = leverage,
iconUrl = iconUrl,
fundingRate = "0.0001",
minAmount = "1",
maxAmount = "100000",
last = "100.00",
volume = "1000000",
high = "120.00",
low = "90.00",
open = "95.00",
change = change,
bidPrice = "99.90",
askPrice = "100.10",
createdAt = "2026-05-26T00:00:00Z",
updatedAt = "2026-05-26T00:00:00Z",
)
}
11 changes: 10 additions & 1 deletion app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<meta-data
android:name="firebase_performance_collection_deactivated"
android:value="true" />
<meta-data
android:name="firebase_performance_collection_enabled"
android:value="false" />

<activity
android:name=".HiltTestActivity"
android:theme="@style/AppTheme.NoActionBar"
android:exported="false" />
</application>

Expand Down
19 changes: 17 additions & 2 deletions app/src/main/java/one/mixin/android/MixinApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import dagger.hilt.components.SingletonComponent
import io.reactivex.plugins.RxJavaPlugins
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -146,12 +147,26 @@ open class MixinApplication :
private var activityReferences: Int = 0
private var isActivityChangingConfigurations = false

@Inject
@ApplicationScope
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ApplicationScopeEntryPoint {
@one.mixin.android.di.ApplicationScope
fun getApplicationScope(): CoroutineScope
}

private fun getAppScope(): CoroutineScope {
return try {
EntryPointAccessors.fromApplication(this, ApplicationScopeEntryPoint::class.java).getApplicationScope()
} catch (e: Exception) {
CoroutineScope(Dispatchers.Main + SupervisorJob())
}
}

lateinit var applicationScope: CoroutineScope

override fun onCreate() {
super.onCreate()
applicationScope = getAppScope()
init()
registerActivityLifecycleCallbacks(this)
SignalProtocolLoggerProvider.setProvider(MixinSignalProtocolLogger())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,8 @@ class TradeFragment : BaseFragment() {
onShowMarketList = { isLong ->
PerpsMarketListBottomSheetDialogFragment.newInstance(isLong).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG)
},
onShowAllMarkets = { initialCategory ->
PerpsMarketListBottomSheetDialogFragment.newInstance(initialCategory).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG)
onShowAllMarkets = { initialCategory, initialSort ->
PerpsMarketListBottomSheetDialogFragment.newInstance(initialCategory, initialSort).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG)
},
onShowAllOpenPositions = {
navTo(AllPositionsFragment.newOpenInstance(), AllPositionsFragment.TAG)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import one.mixin.android.ui.home.web3.components.OutlinedTab
import one.mixin.android.ui.home.web3.components.PageScaffold
import one.mixin.android.ui.home.web3.trade.perps.PerpetualContent
import one.mixin.android.ui.home.web3.trade.perps.PerpetualViewModel
import one.mixin.android.ui.home.web3.widget.MarketSort
import one.mixin.android.util.analytics.AnalyticsTracker
import one.mixin.android.vo.WalletCategory
import java.math.BigDecimal
Expand Down Expand Up @@ -103,7 +104,7 @@ fun TradePage(
onShowTradingGuide: (Int) -> Unit,
onShowHelpBottomSheet: (() -> Unit, () -> Unit) -> Unit,
onShowMarketList: (Boolean) -> Unit,
onShowAllMarkets: (String?) -> Unit,
onShowAllMarkets: (String?, MarketSort?) -> Unit,
onShowAllOpenPositions: () -> Unit,
onShowAllClosedPositions: () -> Unit,
onOpenPositionClick: (PerpsPositionItem) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import one.mixin.android.compose.theme.MixinAppTheme
import one.mixin.android.extension.defaultSharedPreferences
import one.mixin.android.extension.putString
import one.mixin.android.session.Session
import one.mixin.android.ui.home.web3.widget.MarketSort
import one.mixin.android.ui.wallet.alert.components.cardBackground
import one.mixin.android.util.analytics.AnalyticsTracker
import one.mixin.android.widget.components.MixinButton
Expand All @@ -68,7 +69,7 @@ private const val CLOSED_POSITION_PREVIEW_LIMIT = 10
fun PerpetualContent(
onShowTradingGuide: () -> Unit,
onShowMarketList: (isLong: Boolean) -> Unit,
onShowAllMarkets: (String?) -> Unit,
onShowAllMarkets: (String?, MarketSort?) -> Unit,
onShowAllOpenPositions: () -> Unit,
onShowAllClosedPositions: () -> Unit,
onOpenPositionClick: (PerpsPositionItem) -> Unit,
Expand Down Expand Up @@ -102,6 +103,9 @@ fun PerpetualContent(
val openPositionsCount = openPositions.size
val openPositionsPreview = openPositions.take(3)
val marketsPreview = markets.take(3)
val topMoversPreview = remember(markets) {
markets.sortedByDescending { it.changePercent() }.take(8)
}
val sourceOrder = remember(markets) {
markets.withIndex().associate { it.value.marketId to it.index }
}
Expand Down Expand Up @@ -308,6 +312,25 @@ fun PerpetualContent(
}
}

if (topMoversPreview.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
Column(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.clip(RoundedCornerShape(8.dp))
.cardBackground(Color.Transparent, MixinAppTheme.colors.borderColor)
.padding(vertical = 16.dp)
) {
TopMoversCard(
markets = topMoversPreview,
quoteColorReversed = quoteColorReversed,
onViewAllClick = { onShowAllMarkets(null, MarketSort.TWENTY_FOUR_HOURS_PERCENTAGE_DESCENDING) },
onMarketItemClick = onMarketItemClick,
)
}
}

Spacer(modifier = Modifier.height(16.dp))

Column(
Expand All @@ -324,8 +347,8 @@ fun PerpetualContent(
markets = marketsPreview,
totalCount = markets.size,
quoteColorReversed = quoteColorReversed,
onTitleClick = { onShowAllMarkets(null) },
onViewAllClick = { onShowAllMarkets(null) },
onTitleClick = { onShowAllMarkets(null, null) },
onViewAllClick = { onShowAllMarkets(null, null) },
onMarketItemClick = onMarketItemClick,
)
} else if (isLoading) {
Expand Down Expand Up @@ -373,10 +396,10 @@ fun PerpetualContent(
totalCount = stocksMarkets.size,
quoteColorReversed = quoteColorReversed,
onTitleClick = {
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS)
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS, null)
},
onViewAllClick = {
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS)
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS, null)
},
onMarketItemClick = onMarketItemClick,
)
Expand All @@ -399,10 +422,10 @@ fun PerpetualContent(
totalCount = commoditiesMarkets.size,
quoteColorReversed = quoteColorReversed,
onTitleClick = {
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES)
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES, null)
},
onViewAllClick = {
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES)
onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES, null)
},
onMarketItemClick = onMarketItemClick,
)
Expand Down
Loading