mirror of
https://github.com/azahar-emu/azahar
synced 2025-11-06 15:09:58 +01:00
android: Implement play time tracking (#813)
* android: Implement play time tacking Co-Authored-By: Reg Tiangha <rtiangha@users.noreply.github.com> * Moved playtime manager from `citra_qt` to `common` * Reimplemented Android play time to use existing logic from desktop * Updated license headers * When getting current game ID fails, silently error rather than crashing * playTimeManagerStart: Check that `play_time_manager` is initialized before using it --------- Co-authored-by: Kleidis <167202775+kleidis@users.noreply.github.com> Co-authored-by: Reg Tiangha <rtiangha@users.noreply.github.com>
This commit is contained in:
parent
06ed6f3b6d
commit
7c278f9701
@ -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() {
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<TextView>(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<MaterialButton>(R.id.game_shortcut).setOnClickListener {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
|
||||
@ -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 -> {
|
||||
|
||||
@ -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<Common::DynamicLibrary> vulkan_library{};
|
||||
std::unique_ptr<EmuWindow_Android> window;
|
||||
|
||||
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
|
||||
jlong ptm_current_title_id = std::numeric_limits<jlong>::max(); // Arbitrary default value
|
||||
|
||||
std::atomic<bool> stop_run{true};
|
||||
std::atomic<bool> 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<PlayTime::PlayTimeManager>();
|
||||
}
|
||||
|
||||
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<u64>(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<jlong>(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"
|
||||
|
||||
@ -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" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/about_game_playtime"
|
||||
style="?attr/textAppearanceBodyMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@id/about_game_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/about_game_filename"
|
||||
tools:text="Game Playtime" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@ -171,8 +171,6 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL
|
||||
multiplayer/state.cpp
|
||||
multiplayer/state.h
|
||||
multiplayer/validation.h
|
||||
play_time_manager.cpp
|
||||
play_time_manager.h
|
||||
precompiled_headers.h
|
||||
uisettings.cpp
|
||||
uisettings.h
|
||||
|
||||
@ -67,9 +67,9 @@
|
||||
#include "citra_qt/movie/movie_play_dialog.h"
|
||||
#include "citra_qt/movie/movie_record_dialog.h"
|
||||
#include "citra_qt/multiplayer/state.h"
|
||||
#include "citra_qt/play_time_manager.h"
|
||||
#include "citra_qt/qt_image_interface.h"
|
||||
#include "citra_qt/uisettings.h"
|
||||
#include "common/play_time_manager.h"
|
||||
#ifdef ENABLE_QT_UPDATE_CHECKER
|
||||
#include "citra_qt/update_checker.h"
|
||||
#endif
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
#include <QVector>
|
||||
#include <QWidget>
|
||||
#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 {
|
||||
|
||||
@ -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 <algorithm>
|
||||
@ -18,11 +17,11 @@
|
||||
#include <QStandardItem>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
#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);
|
||||
}
|
||||
|
||||
|
||||
@ -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 <QString>
|
||||
#include <QVector>
|
||||
#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;
|
||||
|
||||
@ -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<double>(time_seconds) / 60, 1.0);
|
||||
const auto time_hours = static_cast<double>(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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 <filesystem>
|
||||
#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<double>(time_seconds) / 60, 1.0);
|
||||
const auto time_hours = static_cast<double>(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
|
||||
@ -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 <QString>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "common/common_funcs.h"
|
||||
@ -40,6 +39,4 @@ private:
|
||||
std::jthread play_time_thread;
|
||||
};
|
||||
|
||||
QString ReadablePlayTime(qulonglong time_seconds);
|
||||
|
||||
} // namespace PlayTime
|
||||
Loading…
x
Reference in New Issue
Block a user