Browse Source

[android] fix persist manual game pause after android sleep/wake (#3651)

if user invokes the "pause game" option from the menu while in game, as expected this suspends the process till user manually hits resume.. except for one case: Android sleep/wake lifecycle.

If user manually pauses a running game, then sleeps their device, then wakes their device; the game will self-resume without user pressing "resume game".

Expected behavior IMO is that if user left the game process in manually paused state, app should respect this and persist the pause on system wake, so that user may manually press "resume game" to unfreeze the process.

Simple fix is to have a few params for user initiated pause and resume, and update the pause and run methods to handle as described above.

Please let me know if there is a cleaner way to implement!

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3651
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: xXJSONDeruloXx <danielhimebauch@gmail.com>
Co-committed-by: xXJSONDeruloXx <danielhimebauch@gmail.com>
pull/3657/head
xXJSONDeruloXx 6 days ago
committed by crueter
parent
commit
7de5eb6884
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 4
      src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
  2. 8
      src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
  3. 144
      src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
  4. 7
      src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
  5. 11
      src/android/app/src/main/jni/emu_window/emu_window.cpp
  6. 41
      src/android/app/src/main/jni/native.cpp
  7. 4
      src/android/app/src/main/res/drawable/circle_white.xml
  8. 28
      src/android/app/src/main/res/layout/fragment_emulation.xml

4
src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt

@ -152,6 +152,10 @@ object NativeLibrary {
external fun surfaceDestroyed() external fun surfaceDestroyed()
external fun getAppletCaptureBuffer(): ByteArray
external fun getAppletCaptureWidth(): Int
external fun getAppletCaptureHeight(): Int
/** /**
* Unpauses emulation from a paused state. * Unpauses emulation from a paused state.
*/ */

8
src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@ -204,9 +204,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
} }
override fun onPause() { override fun onPause() {
super.onPause()
nfcReader.stopScanning() nfcReader.stopScanning()
stopMotionSensorListener() stopMotionSensorListener()
super.onPause()
} }
override fun onDestroy() { override fun onDestroy() {
@ -339,6 +339,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
} }
override fun onSensorChanged(event: SensorEvent) { override fun onSensorChanged(event: SensorEvent) {
if (!NativeLibrary.isRunning() || NativeLibrary.isPaused()) {
return
}
val rotation = this.display?.rotation val rotation = this.display?.rotation
if (rotation == Surface.ROTATION_90) { if (rotation == Surface.ROTATION_90) {
flipMotionOrientation = true flipMotionOrientation = true

144
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt

@ -15,6 +15,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.BatteryManager import android.os.BatteryManager
import android.os.BatteryManager.* import android.os.BatteryManager.*
@ -97,6 +98,7 @@ import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.utils.CustomSettingsHandler import org.yuzu.yuzu_emu.utils.CustomSettingsHandler
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.nio.ByteBuffer
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.or import kotlin.or
@ -141,6 +143,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var wasInputOverlayAutoHidden = false private var wasInputOverlayAutoHidden = false
private var overlayTouchActive = false private var overlayTouchActive = false
private var pausedFrameBitmap: Bitmap? = null
var shouldUseCustom = false var shouldUseCustom = false
private var isQuickSettingsMenuOpen = false private var isQuickSettingsMenuOpen = false
@ -703,6 +706,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible = binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible =
BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean() BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()
binding.pausedIcon.setOnClickListener {
if (this::emulationState.isInitialized && emulationState.isPaused) {
resumeEmulationFromUi()
}
}
binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply { binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
val lockMode = IntSetting.LOCK_DRAWER.getInt() val lockMode = IntSetting.LOCK_DRAWER.getInt()
val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) { val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
@ -728,11 +737,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
when (it.itemId) { when (it.itemId) {
R.id.menu_pause_emulation -> { R.id.menu_pause_emulation -> {
if (emulationState.isPaused) { if (emulationState.isPaused) {
emulationState.run(false)
updatePauseMenuEntry(false)
resumeEmulationFromUi()
} else { } else {
emulationState.pause()
updatePauseMenuEntry(true)
pauseEmulationAndCaptureFrame()
} }
binding.inGameMenu.requestFocus() binding.inGameMenu.requestFocus()
true true
@ -826,6 +833,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
R.id.menu_exit -> { R.id.menu_exit -> {
clearPausedFrame()
emulationState.stop() emulationState.stop()
NativeConfig.reloadGlobalConfig() NativeConfig.reloadGlobalConfig()
emulationViewModel.setIsEmulationStopping(true) emulationViewModel.setIsEmulationStopping(true)
@ -1197,6 +1205,71 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
private fun pauseEmulationAndCaptureFrame() {
emulationState.pause()
updatePauseMenuEntry(true)
capturePausedFrameFromCore()
updatePausedFrameVisibility()
}
private fun capturePausedFrameFromCore() {
lifecycleScope.launch(Dispatchers.Default) {
val frameData = NativeLibrary.getAppletCaptureBuffer()
val width = NativeLibrary.getAppletCaptureWidth()
val height = NativeLibrary.getAppletCaptureHeight()
if (frameData.isEmpty() || width <= 0 || height <= 0) {
Log.warning(
"[EmulationFragment] Paused frame capture returned empty/invalid data. " +
"size=${frameData.size}, width=$width, height=$height"
)
return@launch
}
val expectedSize = width * height * 4
if (frameData.size < expectedSize) {
Log.warning(
"[EmulationFragment] Paused frame buffer smaller than expected. " +
"size=${frameData.size}, expected=$expectedSize"
)
return@launch
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(frameData, 0, expectedSize))
withContext(Dispatchers.Main) {
pausedFrameBitmap?.recycle()
pausedFrameBitmap = bitmap
updatePausedFrameVisibility()
}
}
}
private fun updatePausedFrameVisibility() {
val b = _binding ?: return
val showPausedUi = this::emulationState.isInitialized && emulationState.isPaused
b.pausedIcon.setVisible(showPausedUi)
val bitmap = if (showPausedUi) pausedFrameBitmap else null
b.pausedFrameImage.setImageBitmap(bitmap)
b.pausedFrameImage.setVisible(bitmap != null)
}
private fun resumeEmulationFromUi() {
clearPausedFrame()
emulationState.resume()
updatePauseMenuEntry(emulationState.isPaused)
updatePausedFrameVisibility()
}
private fun clearPausedFrame() {
val b = _binding
b?.pausedFrameImage?.setVisible(false)
b?.pausedFrameImage?.setImageDrawable(null)
pausedFrameBitmap?.recycle()
pausedFrameBitmap = null
}
private fun handleLoadAmiiboSelection(): Boolean { private fun handleLoadAmiiboSelection(): Boolean {
val binding = _binding ?: return true val binding = _binding ?: return true
@ -1290,8 +1363,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
override fun onPause() { override fun onPause() {
if (this::emulationState.isInitialized) { if (this::emulationState.isInitialized) {
if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
emulationState.pause()
updatePauseMenuEntry(true)
pauseEmulationAndCaptureFrame()
} else {
updatePausedFrameVisibility()
} }
} }
super.onPause() super.onPause()
@ -1301,6 +1375,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDestroyView() super.onDestroyView()
amiiboLoadJob?.cancel() amiiboLoadJob?.cancel()
amiiboLoadJob = null amiiboLoadJob = null
clearPausedFrame()
_binding?.surfaceInputOverlay?.touchEventListener = null _binding?.surfaceInputOverlay?.touchEventListener = null
_binding = null _binding = null
isAmiiboPickerOpen = false isAmiiboPickerOpen = false
@ -1321,6 +1396,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
b.inGameMenu.post { b.inGameMenu.post {
if (!this::emulationState.isInitialized || _binding == null) return@post if (!this::emulationState.isInitialized || _binding == null) return@post
updatePauseMenuEntry(emulationState.isPaused) updatePauseMenuEntry(emulationState.isPaused)
updatePausedFrameVisibility()
} }
} }
@ -1760,6 +1836,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// Only update surface reference, don't trigger state changes // Only update surface reference, don't trigger state changes
emulationState.updateSurfaceReference(holder.surface) emulationState.updateSurfaceReference(holder.surface)
} }
updatePausedFrameVisibility()
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
@ -2090,6 +2167,29 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
@Synchronized
fun resume() {
if (state != State.PAUSED) {
Log.warning("[EmulationFragment] Resume called while emulation is not paused.")
return
}
if (!emulationCanStart.invoke()) {
Log.warning("[EmulationFragment] Resume blocked by emulationCanStart check.")
return
}
val currentSurface = surface
if (currentSurface == null || !currentSurface.isValid) {
Log.debug("[EmulationFragment] Resume requested with invalid surface.")
return
}
NativeLibrary.surfaceChanged(currentSurface)
Log.debug("[EmulationFragment] Resuming emulation.")
NativeLibrary.unpauseEmulation()
NativeLibrary.playTimeManagerStart()
state = State.RUNNING
}
@Synchronized @Synchronized
fun changeProgram(programIndex: Int) { fun changeProgram(programIndex: Int) {
emulationThread.join() emulationThread.join()
@ -2111,7 +2211,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@Synchronized @Synchronized
fun updateSurface() { fun updateSurface() {
if (surface != null) {
if (surface != null && state == State.RUNNING) {
NativeLibrary.surfaceChanged(surface) NativeLibrary.surfaceChanged(surface)
} }
} }
@ -2127,20 +2227,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@Synchronized @Synchronized
fun clearSurface() { fun clearSurface() {
if (surface == null) { if (surface == null) {
Log.warning("[EmulationFragment] clearSurface called, but surface already null.")
Log.debug("[EmulationFragment] clearSurface called, but surface already null.")
} else { } else {
if (state == State.RUNNING) {
pause()
}
NativeLibrary.surfaceDestroyed()
surface = null surface = null
Log.debug("[EmulationFragment] Surface destroyed.") Log.debug("[EmulationFragment] Surface destroyed.")
when (state) { when (state) {
State.RUNNING -> {
state = State.PAUSED
}
State.PAUSED -> Log.warning(
State.PAUSED -> Log.debug(
"[EmulationFragment] Surface cleared while emulation paused." "[EmulationFragment] Surface cleared while emulation paused."
) )
else -> Log.warning(
else -> Log.debug(
"[EmulationFragment] Surface cleared while emulation stopped." "[EmulationFragment] Surface cleared while emulation stopped."
) )
} }
@ -2148,29 +2248,35 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
private fun runWithValidSurface(programIndex: Int = 0) { private fun runWithValidSurface(programIndex: Int = 0) {
NativeLibrary.surfaceChanged(surface)
if (!emulationCanStart.invoke()) { if (!emulationCanStart.invoke()) {
return return
} }
val currentSurface = surface
if (currentSurface == null || !currentSurface.isValid) {
Log.debug("[EmulationFragment] runWithValidSurface called with invalid surface.")
return
}
when (state) { when (state) {
State.STOPPED -> { State.STOPPED -> {
NativeLibrary.surfaceChanged(currentSurface)
emulationThread = Thread({ emulationThread = Thread({
Log.debug("[EmulationFragment] Starting emulation thread.") Log.debug("[EmulationFragment] Starting emulation thread.")
NativeLibrary.run(gamePath, programIndex, true) NativeLibrary.run(gamePath, programIndex, true)
}, "NativeEmulation") }, "NativeEmulation")
emulationThread.start() emulationThread.start()
state = State.RUNNING
} }
State.PAUSED -> { State.PAUSED -> {
Log.debug("[EmulationFragment] Resuming emulation.")
NativeLibrary.unpauseEmulation()
NativeLibrary.playTimeManagerStart()
Log.debug(
"[EmulationFragment] Surface restored while emulation paused; " +
"waiting for explicit resume."
)
} }
else -> Log.debug("[EmulationFragment] Bug, run called while already running.") else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
} }
state = State.RUNNING
} }
private enum class State { private enum class State {

7
src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay package org.yuzu.yuzu_emu.overlay
@ -20,7 +20,6 @@ import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import android.view.SurfaceView
import android.view.View import android.view.View
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import android.view.WindowInsets import android.view.WindowInsets
@ -42,10 +41,10 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
/** /**
* Draws the interactive input overlay on top of the * Draws the interactive input overlay on top of the
* [SurfaceView] that is rendering emulation.
* emulation rendering surface.
*/ */
class InputOverlay(context: Context, attrs: AttributeSet?) : class InputOverlay(context: Context, attrs: AttributeSet?) :
SurfaceView(context, attrs),
View(context, attrs),
OnTouchListener { OnTouchListener {
private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet() private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet() private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()

11
src/android/app/src/main/jni/emu_window/emu_window.cpp

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
@ -14,6 +17,14 @@
#include "jni/native.h" #include "jni/native.h"
void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
if (!surface) {
LOG_INFO(Frontend, "EmuWindow_Android::OnSurfaceChanged received null surface");
m_window_width = 0;
m_window_height = 0;
window_info.render_surface = nullptr;
return;
}
m_window_width = ANativeWindow_getWidth(surface); m_window_width = ANativeWindow_getWidth(surface);
m_window_height = ANativeWindow_getHeight(surface); m_window_height = ANativeWindow_getHeight(surface);

41
src/android/app/src/main/jni/native.cpp

@ -89,6 +89,8 @@
#include "jni/native.h" #include "jni/native.h"
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/capture.h"
#include "video_core/textures/decoders.h"
#include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h" #include "video_core/vulkan_common/vulkan_surface.h"
#include "video_core/shader_notify.h" #include "video_core/shader_notify.h"
@ -780,9 +782,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jobject i
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jobject instance) { void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jobject instance) {
ANativeWindow_release(EmulationSession::GetInstance().NativeWindow());
if (auto* native_window = EmulationSession::GetInstance().NativeWindow(); native_window) {
ANativeWindow_release(native_window);
}
EmulationSession::GetInstance().SetNativeWindow(nullptr); EmulationSession::GetInstance().SetNativeWindow(nullptr);
EmulationSession::GetInstance().SurfaceChanged();
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject instance, void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject instance,
@ -969,6 +972,40 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
} }
jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureBuffer(JNIEnv* env, jclass clazz) {
using namespace VideoCore::Capture;
if (!EmulationSession::GetInstance().IsRunning()) {
return env->NewByteArray(0);
}
const auto tiled = EmulationSession::GetInstance().System().GPU().GetAppletCaptureBuffer();
if (tiled.size() < TiledSize) {
return env->NewByteArray(0);
}
std::vector<u8> linear(LinearWidth * LinearHeight * BytesPerPixel);
Tegra::Texture::UnswizzleTexture(linear, tiled, BytesPerPixel, LinearWidth, LinearHeight,
LinearDepth, BlockHeight, BlockDepth);
auto buffer = env->NewByteArray(static_cast<jsize>(linear.size()));
if (!buffer) {
return env->NewByteArray(0);
}
env->SetByteArrayRegion(buffer, 0, static_cast<jsize>(linear.size()),
reinterpret_cast<const jbyte*>(linear.data()));
return buffer;
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureWidth(JNIEnv* env, jclass clazz) {
return static_cast<jint>(VideoCore::Capture::LinearWidth);
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureHeight(JNIEnv* env, jclass clazz) {
return static_cast<jint>(VideoCore::Capture::LinearHeight);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
jboolean reload) { jboolean reload) {
// Initialize the emulated system. // Initialize the emulated system.

4
src/android/app/src/main/res/drawable/circle_white.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#E6FFFFFF" />
</shape>

28
src/android/app/src/main/res/layout/fragment_emulation.xml

@ -108,6 +108,22 @@
</FrameLayout> </FrameLayout>
<FrameLayout
android:id="@+id/paused_frame_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<ImageView
android:id="@+id/paused_frame_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:visibility="gone" />
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/input_container" android:id="@+id/input_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -142,6 +158,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="false"> android:fitsSystemWindows="false">
<ImageView
android:id="@+id/paused_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@drawable/circle_white"
android:contentDescription="@string/emulation_unpause"
android:padding="14dp"
android:src="@drawable/ic_play"
android:visibility="gone"
app:tint="@android:color/black" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/show_stats_overlay_text" android:id="@+id/show_stats_overlay_text"
style="@style/TextAppearance.Material3.BodySmall" style="@style/TextAppearance.Material3.BodySmall"

Loading…
Cancel
Save