mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-10 22:48:56 +02:00
[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>
This commit is contained in:
parent
9c7ed0f59d
commit
6bdf479488
23 changed files with 586 additions and 45 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class YuzuApplication : Application() {
|
|||
application = this
|
||||
documentsTree = DocumentsTree()
|
||||
DirectoryInitialization.start()
|
||||
NativeLibrary.playTimeManagerInit()
|
||||
GpuDriverHelper.initializeDriverParameters()
|
||||
NativeInput.reloadInputDevices()
|
||||
NativeLibrary.logDeviceInfo()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
59
src/android/app/src/main/res/layout/dialog_edit_playtime.xml
Normal file
59
src/android/app/src/main/res/layout/dialog_edit_playtime.xml
Normal file
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue