diff options
| author | inix <Nixy01@proton.me> | 2025-10-17 22:47:43 +0200 |
|---|---|---|
| committer | crueter <crueter@eden-emu.dev> | 2025-10-17 22:47:43 +0200 |
| commit | 6bdf479488dd87a35bf5ccf9ec334f49bf43ffc7 (patch) | |
| tree | 166376be36272e3177cc2fd474cc24c7eb9d1360 /src/android | |
| parent | 9c7ed0f59dd41ec0bbbfbc81678209ae60a36016 (diff) | |
[core, android] Initial playtime implementation (#2535)
So firstly, playtime code is moved to src/common and qt specific code to yuzu/utils.cpp.
The dependency on ProfileManager was removed because it was working properly on Android, and I think a shared playtime is better behavior.
Now, playtime is stored in a file called "playtime.bin".
JNI code is from Azahar although modified by me, as well as that I added code to reset the game's playtime which was missing for some reason on there.
Before this gets merged, I plan to add the ability to manually edit the database as well.
Note: Code still needs a bit of cleanup.
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2535
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: inix <Nixy01@proton.me>
Co-committed-by: inix <Nixy01@proton.me>
Diffstat (limited to 'src/android')
10 files changed, 300 insertions, 4 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index ba50bcad34..ef8e2bafe9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -206,6 +206,17 @@ object NativeLibrary { ErrorUnknown } + /** + * playtime tracking + */ + external fun playTimeManagerInit() + external fun playTimeManagerStart() + external fun playTimeManagerStop() + external fun playTimeManagerGetPlayTime(programId: String): Long + external fun playTimeManagerGetCurrentTitleId(): Long + external fun playTimeManagerResetProgramPlayTime(programId: String) + external fun playTimeManagerSetPlayTime(programId: String, playTimeSeconds: Long) + var coreErrorAlertResult = false val coreErrorAlertLock = Object() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index e9a0d5a75c..daea2b0370 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -53,6 +53,7 @@ class YuzuApplication : Application() { application = this documentsTree = DocumentsTree() DirectoryInitialization.start() + NativeLibrary.playTimeManagerInit() GpuDriverHelper.initializeDriverParameters() NativeInput.reloadInputDevices() NativeLibrary.logDeviceInfo() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index ba60aeace2..cee68689c3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -61,6 +61,7 @@ import org.yuzu.yuzu_emu.utils.ThemeHelper import java.text.NumberFormat import kotlin.math.roundToInt import org.yuzu.yuzu_emu.utils.ForegroundService +import androidx.core.os.BundleCompat class EmulationActivity : AppCompatActivity(), SensorEventListener { private lateinit var binding: ActivityEmulationBinding @@ -326,6 +327,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { override fun onAccuracyChanged(sensor: Sensor, i: Int) {} + override fun onDestroy() { + super.onDestroy() + NativeLibrary.playTimeManagerStop() + } + private fun enableFullscreenImmersive() { WindowCompat.setDecorFitsSystemWindows(window, false) @@ -530,6 +536,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { fun onEmulationStarted() { emulationViewModel.setEmulationStarted(true) + NativeLibrary.playTimeManagerStart() + } fun onEmulationStopped(status: Int) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index b2d6135372..5f49399406 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1635,6 +1635,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { Log.debug("[EmulationFragment] Pausing emulation.") NativeLibrary.pauseEmulation() + NativeLibrary.playTimeManagerStop() state = State.PAUSED } else { @@ -1725,6 +1726,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { State.PAUSED -> { Log.debug("[EmulationFragment] Resuming emulation.") NativeLibrary.unpauseEmulation() + NativeLibrary.playTimeManagerStart() } else -> Log.debug("[EmulationFragment] Bug, run called while already running.") diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index 6979409cad..bc6272b857 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -1,9 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: 2025 Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.fragments import android.content.Intent @@ -31,6 +28,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.HomeNavigationDirections +import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter @@ -112,6 +110,8 @@ class GamePropertiesFragment : Fragment() { binding.title.text = args.game.title binding.title.marquee() + getPlayTime() + binding.buttonStart.setOnClickListener { LaunchGameDialogFragment.newInstance(args.game) .show(childFragmentManager, LaunchGameDialogFragment.TAG) @@ -136,6 +136,109 @@ class GamePropertiesFragment : Fragment() { gamesViewModel.reloadGames(true) } + private fun getPlayTime() { + binding.playtime.text = buildString { + val playTimeSeconds = NativeLibrary.playTimeManagerGetPlayTime(args.game.programId) + + val hours = playTimeSeconds / 3600 + val minutes = (playTimeSeconds % 3600) / 60 + val seconds = playTimeSeconds % 60 + + val readablePlayTime = when { + hours > 0 -> "${hours}h ${minutes}m ${seconds}s" + minutes > 0 -> "${minutes}m ${seconds}s" + else -> "${seconds}s" + } + + append(getString(R.string.playtime)) + append(readablePlayTime) + } + + binding.playtime.setOnClickListener { + showEditPlaytimeDialog() + } + } + + private fun showEditPlaytimeDialog() { + val dialogView = layoutInflater.inflate(R.layout.dialog_edit_playtime, null) + val hoursLayout = + dialogView.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.layout_hours) + val minutesLayout = + dialogView.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.layout_minutes) + val secondsLayout = + dialogView.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.layout_seconds) + val hoursInput = + dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.input_hours) + val minutesInput = + dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.input_minutes) + val secondsInput = + dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.input_seconds) + + val playTimeSeconds = NativeLibrary.playTimeManagerGetPlayTime(args.game.programId) + val hours = playTimeSeconds / 3600 + val minutes = (playTimeSeconds % 3600) / 60 + val seconds = playTimeSeconds % 60 + + hoursInput.setText(hours.toString()) + minutesInput.setText(minutes.toString()) + secondsInput.setText(seconds.toString()) + + val dialog = com.google.android.material.dialog.MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.edit_playtime) + .setView(dialogView) + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(android.R.string.cancel, null) + .create() + + dialog.setOnShowListener { + val positiveButton = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE) + positiveButton.setOnClickListener { + hoursLayout.error = null + minutesLayout.error = null + secondsLayout.error = null + + val hoursText = hoursInput.text.toString() + val minutesText = minutesInput.text.toString() + val secondsText = secondsInput.text.toString() + + val hoursValue = hoursText.toLongOrNull() ?: 0 + val minutesValue = minutesText.toLongOrNull() ?: 0 + val secondsValue = secondsText.toLongOrNull() ?: 0 + + var hasError = false + + // normally cant be above 9999 + if (hoursValue < 0 || hoursValue > 9999) { + hoursLayout.error = getString(R.string.hours_must_be_between_0_and_9999) + hasError = true + } + + if (minutesValue < 0 || minutesValue > 59) { + minutesLayout.error = getString(R.string.minutes_must_be_between_0_and_59) + hasError = true + } + + if (secondsValue < 0 || secondsValue > 59) { + secondsLayout.error = getString(R.string.seconds_must_be_between_0_and_59) + hasError = true + } + + if (!hasError) { + val totalSeconds = hoursValue * 3600 + minutesValue * 60 + secondsValue + NativeLibrary.playTimeManagerSetPlayTime(args.game.programId, totalSeconds) + getPlayTime() + Toast.makeText( + requireContext(), + R.string.playtime_updated_successfully, + Toast.LENGTH_SHORT + ).show() + dialog.dismiss() + } + } + } + + dialog.show() + } private fun reloadList() { _binding ?: return @@ -324,6 +427,31 @@ class GamePropertiesFragment : Fragment() { ) ) } + if (NativeLibrary.playTimeManagerGetPlayTime(args.game.programId) > 0) { + add( + SubmenuProperty( + R.string.reset_playtime, + R.string.reset_playtime_description, + R.drawable.ic_delete + ) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.reset_playtime, + descriptionId = R.string.reset_playtime_warning_description, + positiveAction = { + NativeLibrary.playTimeManagerResetProgramPlayTime( args.game.programId) + Toast.makeText( + YuzuApplication.appContext, + R.string.playtime_reset_successfully, + Toast.LENGTH_SHORT + ).show() + getPlayTime() + homeViewModel.reloadPropertiesList(true) + } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + ) + } } } binding.listProperties.apply { @@ -336,6 +464,7 @@ class GamePropertiesFragment : Fragment() { override fun onResume() { super.onResume() driverViewModel.updateDriverNameForGame(args.game) + getPlayTime() reloadList() } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 306b7e2a4c..78f0ec1521 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -36,6 +36,7 @@ #include "common/scope_exit.h" #include "common/settings.h" #include "common/string_util.h" +#include "frontend_common/play_time_manager.h" #include "core/core.h" #include "core/cpu_manager.h" #include "core/crypto/key_manager.h" @@ -85,6 +86,9 @@ std::atomic<int> g_battery_percentage = {100}; std::atomic<bool> g_is_charging = {false}; std::atomic<bool> g_has_battery = {true}; +// playtime +std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager; + EmulationSession::EmulationSession() { m_vfs = std::make_shared<FileSys::RealVfsFilesystem>(); } @@ -733,6 +737,56 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* } } +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerInit(JNIEnv* env, jobject obj) { + // for some reason the full user directory isnt initialized in Android, so we need to create it + const auto play_time_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::PlayTimeDir); + if (!Common::FS::IsDir(play_time_dir)) { + if (!Common::FS::CreateDir(play_time_dir)) { + LOG_WARNING(Frontend, "Failed to create play time directory"); + } + } + + play_time_manager = std::make_unique<PlayTime::PlayTimeManager>(); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStart(JNIEnv* env, jobject obj) { + if (play_time_manager) { + play_time_manager->SetProgramId(EmulationSession::GetInstance().System().GetApplicationProcessProgramID()); + play_time_manager->Start(); + } +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStop(JNIEnv* env, jobject obj) { + play_time_manager->Stop(); +} + +jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetPlayTime(JNIEnv* env, jobject obj, + jstring jprogramId) { + u64 program_id = EmulationSession::GetProgramId(env, jprogramId); + return play_time_manager->GetPlayTime(program_id); +} + +jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetCurrentTitleId(JNIEnv* env, + jobject obj) { + return EmulationSession::GetInstance().System().GetApplicationProcessProgramID(); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerResetProgramPlayTime(JNIEnv* env, jobject obj, + jstring jprogramId) { + u64 program_id = EmulationSession::GetProgramId(env, jprogramId); + if (play_time_manager) { + play_time_manager->ResetProgramPlayTime(program_id); + } +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerSetPlayTime(JNIEnv* env, jobject obj, + jstring jprogramId, jlong playTimeSeconds) { + u64 program_id = EmulationSession::GetProgramId(env, jprogramId); + if (play_time_manager) { + play_time_manager->SetPlayTime(program_id, static_cast<u64>(playTimeSeconds)); + } +} + jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, jclass clazz, jlong jid) { auto bis_system = diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml index 7cdef569f6..0fa2fa2a29 100644 --- a/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml +++ b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml @@ -105,6 +105,16 @@ android:textAlignment="center" tools:text="deko_basic" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/playtime" + style="?attr/textAppearanceBodyMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginBottom="8dp" + android:textAlignment="center" + tools:text="Game Playtime" /> + </LinearLayout> <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton diff --git a/src/android/app/src/main/res/layout/dialog_edit_playtime.xml b/src/android/app/src/main/res/layout/dialog_edit_playtime.xml new file mode 100644 index 0000000000..e20508125f --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_edit_playtime.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="16dp"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/layout_hours" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginEnd="8dp" + android:hint="@string/hours" + app:boxBackgroundMode="outline"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/input_hours" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="number" + android:maxLength="4" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/layout_minutes" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginEnd="8dp" + android:hint="@string/minutes" + app:boxBackgroundMode="outline"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/input_minutes" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="number" + android:maxLength="2" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/layout_seconds" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:hint="@string/seconds" + app:boxBackgroundMode="outline"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/input_seconds" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="number" + android:maxLength="2" /> + </com.google.android.material.textfield.TextInputLayout> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml index 5e3f3cf280..9a3437404f 100644 --- a/src/android/app/src/main/res/layout/fragment_game_properties.xml +++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml @@ -74,12 +74,22 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:layout_marginBottom="12dp" + android:layout_marginBottom="2dp" android:layout_marginHorizontal="16dp" android:requiresFadingEdge="horizontal" android:textAlignment="center" tools:text="deko_basic" /> + <com.google.android.material.textview.MaterialTextView + android:id="@+id/playtime" + style="?attr/textAppearanceBodyMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + android:textAlignment="center" + tools:text="Game Playtime" /> + + <androidx.recyclerview.widget.RecyclerView android:id="@+id/list_properties" android:layout_width="match_parent" diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index fd1e7d42fc..37f215e871 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -756,6 +756,18 @@ <string name="copy_details">Copy details</string> <string name="add_ons">Add-ons</string> <string name="add_ons_description">Toggle mods, updates and DLC</string> + <string name="playtime">Playtime:</string> + <string name="reset_playtime">Clear Playtime</string> + <string name="reset_playtime_description">Reset the current game\'s playtime back to 0 seconds</string> + <string name="reset_playtime_warning_description">This will clear the current game\'s playtime data. Are you sure?</string> + <string name="playtime_reset_successfully">Playtime has been reset</string> + <string name="edit_playtime">Edit Playtime</string> + <string name="hours">Hours</string> + <string name="minutes">Minutes</string> + <string name="hours_must_be_between_0_and_9999">Hours must be between 0 and 9999</string> + <string name="minutes_must_be_between_0_and_59">Minutes must be between 0 and 59</string> + <string name="seconds_must_be_between_0_and_59">Seconds must be between 0 and 59</string> + <string name="playtime_updated_successfully">Playtime updated successfully</string> <string name="clear_shader_cache">Clear shader cache</string> <string name="clear_shader_cache_description">Removes all shaders built while playing this game</string> <string name="clear_shader_cache_warning_description">You will experience more stuttering as the shader cache regenerates</string> |
