From a1691a4031ff3e596273351ac39a274c76f472fc Mon Sep 17 00:00:00 2001 From: Shawn Hoffman Date: Tue, 15 Apr 2025 20:36:33 -0700 Subject: [PATCH] Prevent android generating duplicate analytics events dolphin-start event was being generated twice for the normal end-user case, as can be seen in analytics data for some years. The problem occured when: * Android reaped the process hosting the dolphin activity (e.g. for power/memory saving). and * Dolphin activity was in "stopped" state for > 6 hours before being switched back to. Under above conditions, both calls to ReportStartToAnalytics would be performed, as dolphin thought it was being launched anew, and also thought it had been asleep for > 6 hours. fixes https://bugs.dolphin-emu.org/issues/13675 --- .../dolphinemu/ui/main/MainActivity.kt | 7 ---- .../dolphinemu/ui/main/TvMainActivity.kt | 6 ---- .../dolphinemu/utils/ActivityTracker.kt | 29 ++++++++++++--- .../utils/DirectoryInitialization.java | 1 - .../dolphinemu/utils/StartupHandler.java | 35 ++++++++++++------- 5 files changed, 47 insertions(+), 31 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt index a4e5d3600a..7d32f8b56a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt @@ -103,11 +103,6 @@ class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProv presenter.onResume() } - override fun onStart() { - super.onStart() - StartupHandler.checkSessionReset(this) - } - override fun onStop() { super.onStop() if (isChangingConfigurations) { @@ -116,8 +111,6 @@ class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProv // If the currently selected platform tab changed, save it to disk NativeConfig.save(NativeConfig.LAYER_BASE) } - - StartupHandler.setSessionTime(this) } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt index 9beec74fc3..6a0542d483 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt @@ -76,17 +76,11 @@ class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener { presenter.onResume() } - override fun onStart() { - super.onStart() - StartupHandler.checkSessionReset(this) - } - override fun onStop() { super.onStop() if (isChangingConfigurations) { MainPresenter.skipRescanningLibrary() } - StartupHandler.setSessionTime(this) } private fun setupUI() { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt index b3a6a5d91a..83a34e3e8c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt @@ -3,14 +3,29 @@ package org.dolphinemu.dolphinemu.utils import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle +import org.dolphinemu.dolphinemu.ui.main.MainView class ActivityTracker : ActivityLifecycleCallbacks { - val resumedActivities = HashSet() - var backgroundExecutionAllowed = false + private val resumedActivities = HashSet() + private var backgroundExecutionAllowed = false + private var firstStart = true - override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} + private fun isMainActivity(activity: Activity): Boolean { + return activity is MainView + } - override fun onActivityStarted(activity: Activity) {} + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + if (isMainActivity(activity)) { + firstStart = bundle == null + } + } + + override fun onActivityStarted(activity: Activity) { + if (isMainActivity(activity)) { + StartupHandler.reportStartToAnalytics(activity.applicationContext, firstStart) + firstStart = false + } + } override fun onActivityResumed(activity: Activity) { resumedActivities.add(activity) @@ -28,7 +43,11 @@ class ActivityTracker : ActivityLifecycleCallbacks { } } - override fun onActivityStopped(activity: Activity) {} + override fun onActivityStopped(activity: Activity) { + if (isMainActivity(activity)) { + StartupHandler.updateSessionTimestamp(activity.applicationContext) + } + } override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java index 1d7342372f..2942bfe29f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java @@ -84,7 +84,6 @@ public final class DirectoryInitialization extractSysDirectory(context); NativeLibrary.Initialize(); - NativeLibrary.ReportStartToAnalytics(); areDirectoriesAvailable = true; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java index 63b8657f83..db003b82b2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.utils; +import android.app.Activity; import android.content.ClipData; import android.content.Context; import android.content.Intent; @@ -11,6 +12,7 @@ import android.os.Bundle; import android.text.TextUtils; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceManager; import org.dolphinemu.dolphinemu.NativeLibrary; @@ -22,7 +24,7 @@ import java.util.Objects; public final class StartupHandler { - public static final String LAST_CLOSED = "LAST_CLOSED"; + private static final String SESSION_TIMESTAMP = "SESSION_TIMESTAMP"; public static void HandleInit(FragmentActivity parent) { @@ -88,29 +90,38 @@ public final class StartupHandler return null; } + private static Instant getSessionTimestamp(Context context) + { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + long timestamp = preferences.getLong(SESSION_TIMESTAMP, 0); + return Instant.ofEpochMilli(timestamp); + } + /** - * There isn't a good way to determine a new session. setSessionTime is called if the main - * activity goes into the background. + * Called on activity stop / to set timestamp to "now". */ - public static void setSessionTime(Context context) + public static void updateSessionTimestamp(Context context) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor sPrefsEditor = preferences.edit(); - sPrefsEditor.putLong(LAST_CLOSED, System.currentTimeMillis()); + sPrefsEditor.putLong(SESSION_TIMESTAMP, Instant.now().toEpochMilli()); sPrefsEditor.apply(); } /** - * Called to determine if we treat this activity start as a new session. + * Called on activity start. Generates analytics start event if it's a fresh start of the app, or + * if it's a start after a long period of the app not being used (during which time the process + * may be restarted for power/memory saving reasons, although app state persists). */ - public static void checkSessionReset(Context context) + public static void reportStartToAnalytics(Context context, boolean firstStart) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - long lastOpen = preferences.getLong(LAST_CLOSED, 0); - final Instant current = Instant.now(); - final Instant lastOpened = Instant.ofEpochMilli(lastOpen); - if (current.isAfter(lastOpened.plus(6, ChronoUnit.HOURS))) + final Instant sessionTimestamp = getSessionTimestamp(context); + final Instant now = Instant.now(); + if (firstStart || now.isAfter(sessionTimestamp.plus(6, ChronoUnit.HOURS))) { + // Just in case: ensure start event won't be accidentally sent too often. + updateSessionTimestamp(context); + new AfterDirectoryInitializationRunner().runWithoutLifecycle( NativeLibrary::ReportStartToAnalytics); }