diff --git a/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.kt b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.kt index 75a88baf1..c4db63928 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -58,6 +58,7 @@ class CitraApplication : Application() { NativeLibrary.logDeviceInfo() logDeviceInfo() createNotificationChannel() + NativeLibrary.playTimeManagerInit() } fun logDeviceInfo() { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 3e62b5d61..6d93725a4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -21,7 +21,6 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.citra.citra_emu.activities.EmulationActivity -import org.citra.citra_emu.utils.EmulationMenuSettings import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.Log import java.lang.ref.WeakReference @@ -190,6 +189,12 @@ object NativeLibrary { external fun disableTemporaryFrameLimit() + external fun playTimeManagerInit() + external fun playTimeManagerStart(titleId: Long) + external fun playTimeManagerStop() + external fun playTimeManagerGetPlayTime(titleId: Long): Long + external fun playTimeManagerGetCurrentTitleId(): Long + private var coreErrorAlertResult = false private val coreErrorAlertLock = Object() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index 35c7d0474..9f59ea2c4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -6,7 +6,6 @@ package org.citra.citra_emu.activities import android.Manifest.permission import android.annotation.SuppressLint -import android.app.Activity import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager @@ -21,6 +20,7 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.BundleCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -40,12 +40,13 @@ import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.fragments.EmulationFragment import org.citra.citra_emu.fragments.MessageDialogFragment +import org.citra.citra_emu.model.Game import org.citra.citra_emu.utils.ControllerMappingHelper import org.citra.citra_emu.utils.FileBrowserHelper import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.EmulationMenuSettings +import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.ThemeUtil -import org.citra.citra_emu.utils.TurboHelper import org.citra.citra_emu.viewmodel.EmulationViewModel class EmulationActivity : AppCompatActivity() { @@ -110,6 +111,20 @@ class EmulationActivity : AppCompatActivity() { instance = this applyOrientationSettings() // Check for orientation settings at startup + + val game = try { + intent.extras?.let { extras -> + BundleCompat.getParcelable(extras, "game", Game::class.java) + } ?: run { + Log.error("[EmulationActivity] Missing game data in intent extras") + return + } + } catch (e: Exception) { + Log.error("[EmulationActivity] Failed to retrieve game data: ${e.message}") + return + } + + NativeLibrary.playTimeManagerStart(game.titleId) } // On some devices, the system bars will not disappear on first boot or after some @@ -143,6 +158,7 @@ class EmulationActivity : AppCompatActivity() { override fun onDestroy() { EmulationLifecycleUtil.clear() + NativeLibrary.playTimeManagerStop() isEmulationRunning = false instance = null super.onDestroy() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt index f99056f47..f43165a8a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt @@ -45,6 +45,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.CoroutineScope import org.citra.citra_emu.HomeNavigationDirections import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder import org.citra.citra_emu.databinding.CardGameBinding @@ -351,6 +352,24 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater: view.findNavController().navigate(action) } + bottomSheetView.findViewById(R.id.about_game_playtime).text = + buildString { + val playTimeSeconds = NativeLibrary.playTimeManagerGetPlayTime(game.titleId) + + 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("Playtime: ") + append(readablePlayTime) + } + bottomSheetView.findViewById(R.id.game_shortcut).setOnClickListener { val preferences = PreferenceManager.getDefaultSharedPreferences(context) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 18cf2c4ed..ae170c1c7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -355,7 +355,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } R.id.menu_exit -> { - NativeLibrary.pauseEmulation() + emulationState.pause() MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.emulation_close_game) .setMessage(R.string.emulation_close_game_message) @@ -363,9 +363,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram EmulationLifecycleUtil.closeGame() } .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> - NativeLibrary.unPauseEmulation() + emulationState.unpause() } - .setOnCancelListener { NativeLibrary.unPauseEmulation() } + .setOnCancelListener { emulationState.unpause() } .show() true } @@ -470,7 +470,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram super.onResume() Choreographer.getInstance().postFrameCallback(this) if (NativeLibrary.isRunning()) { - NativeLibrary.unPauseEmulation() + emulationState.pause() // If the overlay is enabled, we need to update the position if changed val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int @@ -1395,6 +1395,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram // Release the surface before pausing, since emulation has to be running for that. NativeLibrary.surfaceDestroyed() NativeLibrary.pauseEmulation() + NativeLibrary.playTimeManagerStop() } else { Log.warning("[EmulationFragment] Pause called while already paused.") } @@ -1407,6 +1408,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram Log.debug("[EmulationFragment] Unpausing emulation.") NativeLibrary.unPauseEmulation() + NativeLibrary.playTimeManagerStart(NativeLibrary.playTimeManagerGetCurrentTitleId()) } else { Log.warning("[EmulationFragment] Unpause called while already running.") } @@ -1473,7 +1475,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram State.PAUSED -> { Log.debug("[EmulationFragment] Resuming emulation.") - NativeLibrary.unPauseEmulation() + unpause() } else -> { diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 71c40a1fa..9fddc5c32 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -27,6 +27,7 @@ #include "common/logging/backend.h" #include "common/logging/log.h" #include "common/microprofile.h" +#include "common/play_time_manager.h" #include "common/scm_rev.h" #include "common/scope_exit.h" #include "common/settings.h" @@ -71,6 +72,9 @@ ANativeWindow* s_surf; std::shared_ptr vulkan_library{}; std::unique_ptr window; +std::unique_ptr play_time_manager; +jlong ptm_current_title_id = std::numeric_limits::max(); // Arbitrary default value + std::atomic stop_run{true}; std::atomic pause_emulation{false}; @@ -795,4 +799,31 @@ void Java_org_citra_citra_1emu_NativeLibrary_disableTemporaryFrameLimit(JNIEnv* Settings::is_temporary_frame_limit = false; } +void Java_org_citra_citra_1emu_NativeLibrary_playTimeManagerInit(JNIEnv* env, jobject obj) { + play_time_manager = std::make_unique(); +} + +void Java_org_citra_citra_1emu_NativeLibrary_playTimeManagerStart(JNIEnv* env, jobject obj, + jlong title_id) { + ptm_current_title_id = title_id; + if (play_time_manager) { + play_time_manager->SetProgramId(static_cast(title_id)); + play_time_manager->Start(); + } +} + +void Java_org_citra_citra_1emu_NativeLibrary_playTimeManagerStop(JNIEnv* env, jobject obj) { + play_time_manager->Stop(); +} + +jlong Java_org_citra_citra_1emu_NativeLibrary_playTimeManagerGetPlayTime(JNIEnv* env, jobject obj, + jlong title_id) { + return static_cast(play_time_manager->GetPlayTime(title_id)); +} + +jlong Java_org_citra_citra_1emu_NativeLibrary_playTimeManagerGetCurrentTitleId(JNIEnv* env, + jobject obj) { + return ptm_current_title_id; +} + } // extern "C" diff --git a/src/android/app/src/main/res/layout/dialog_about_game.xml b/src/android/app/src/main/res/layout/dialog_about_game.xml index 32aa26339..76df39868 100644 --- a/src/android/app/src/main/res/layout/dialog_about_game.xml +++ b/src/android/app/src/main/res/layout/dialog_about_game.xml @@ -44,8 +44,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAlignment="viewStart" - android:textSize="20sp" - app:layout_constraintStart_toStartOf="parent" + android:textSize="15sp" + android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Application Title" /> @@ -85,6 +85,15 @@ app:layout_constraintTop_toBottomOf="@+id/about_game_id" tools:text="Application Filename" /> + + #include #include "citra_qt/compatibility_list.h" -#include "citra_qt/play_time_manager.h" #include "common/common_types.h" +#include "common/play_time_manager.h" #include "uisettings.h" namespace Service::FS { diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index 09dc5ab4e..046ec7267 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -1,7 +1,6 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. - #pragma once #include @@ -18,11 +17,11 @@ #include #include #include -#include "citra_qt/play_time_manager.h" #include "citra_qt/uisettings.h" #include "citra_qt/util/util.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "common/play_time_manager.h" #include "common/string_util.h" #include "core/loader/smdh.h" @@ -382,7 +381,7 @@ public: void setData(const QVariant& value, int role) override { qulonglong time_seconds = value.toULongLong(); - GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole); + GameListItem::setData(ReadableDuration(time_seconds), Qt::DisplayRole); GameListItem::setData(value, PlayTimeRole); } diff --git a/src/citra_qt/game_list_worker.h b/src/citra_qt/game_list_worker.h index 09da74f4f..f149a7924 100644 --- a/src/citra_qt/game_list_worker.h +++ b/src/citra_qt/game_list_worker.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2018 yuzu emulator team // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -13,8 +17,8 @@ #include #include #include "citra_qt/compatibility_list.h" -#include "citra_qt/play_time_manager.h" #include "common/common_types.h" +#include "common/play_time_manager.h" namespace Service::FS { enum class MediaType : u32; diff --git a/src/citra_qt/util/util.cpp b/src/citra_qt/util/util.cpp index 9c234ec37..39fdcf1cb 100644 --- a/src/citra_qt/util/util.cpp +++ b/src/citra_qt/util/util.cpp @@ -36,6 +36,21 @@ QString ReadableByteSize(qulonglong size) { .arg(QString::fromUtf8(units[digit_groups])); } +QString ReadableDuration(qulonglong time_seconds) { + if (time_seconds == 0) { + return {}; + } + const auto time_minutes = std::max(static_cast(time_seconds) / 60, 1.0); + const auto time_hours = static_cast(time_seconds) / 3600; + const bool is_minutes = time_minutes < 60; + const char* unit = is_minutes ? "m" : "h"; + const auto value = is_minutes ? time_minutes : time_hours; + + return QStringLiteral("%L1 %2") + .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0) + .arg(QString::fromUtf8(unit)); +} + QPixmap CreateCirclePixmapFromColor(const QColor& color) { QPixmap circle_pixmap(16, 16); circle_pixmap.fill(Qt::transparent); diff --git a/src/citra_qt/util/util.h b/src/citra_qt/util/util.h index fff56166f..01d928eae 100644 --- a/src/citra_qt/util/util.h +++ b/src/citra_qt/util/util.h @@ -15,6 +15,9 @@ QFont GetMonospaceFont(); /// Convert a size in bytes into a readable format (KiB, MiB, etc.) QString ReadableByteSize(qulonglong size); +// Converts a length of time in seconds into a readable format +QString ReadableDuration(qulonglong time_seconds); + /** * Creates a circle pixmap from a specified color * @param color The color the pixmap shall have diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 113eb6b7e..44f0a67dc 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -114,6 +114,8 @@ add_library(citra_common STATIC microprofileui.h param_package.cpp param_package.h + play_time_manager.cpp + play_time_manager.h polyfill_thread.h precompiled_headers.h quaternion.h diff --git a/src/citra_qt/play_time_manager.cpp b/src/common/play_time_manager.cpp similarity index 84% rename from src/citra_qt/play_time_manager.cpp rename to src/common/play_time_manager.cpp index 03c2e1b16..c8b94ee08 100644 --- a/src/citra_qt/play_time_manager.cpp +++ b/src/common/play_time_manager.cpp @@ -1,12 +1,13 @@ -// SPDX-FileCopyrightText: 2024 Citra Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. #include -#include "citra_qt/play_time_manager.h" #include "common/alignment.h" #include "common/common_paths.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "common/play_time_manager.h" #include "common/settings.h" #include "common/thread.h" @@ -140,19 +141,4 @@ void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { Save(); } -QString ReadablePlayTime(qulonglong time_seconds) { - if (time_seconds == 0) { - return {}; - } - const auto time_minutes = std::max(static_cast(time_seconds) / 60, 1.0); - const auto time_hours = static_cast(time_seconds) / 3600; - const bool is_minutes = time_minutes < 60; - const char* unit = is_minutes ? "m" : "h"; - const auto value = is_minutes ? time_minutes : time_hours; - - return QStringLiteral("%L1 %2") - .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0) - .arg(QString::fromUtf8(unit)); -} - } // namespace PlayTime diff --git a/src/citra_qt/play_time_manager.h b/src/common/play_time_manager.h similarity index 82% rename from src/citra_qt/play_time_manager.h rename to src/common/play_time_manager.h index c8ba48db7..6c6fc4094 100644 --- a/src/citra_qt/play_time_manager.h +++ b/src/common/play_time_manager.h @@ -1,10 +1,9 @@ -// SPDX-FileCopyrightText: 2024 Citra Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. #pragma once -#include - #include #include "common/common_funcs.h" @@ -40,6 +39,4 @@ private: std::jthread play_time_thread; }; -QString ReadablePlayTime(qulonglong time_seconds); - } // namespace PlayTime