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
This commit is contained in:
Shawn Hoffman 2025-04-15 20:36:33 -07:00
parent 4f210df86a
commit 257ce10232
5 changed files with 52 additions and 34 deletions

View File

@ -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 {

View File

@ -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() {

View File

@ -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<Activity>()
var backgroundExecutionAllowed = false
private val resumedActivities = HashSet<Activity>()
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, 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)
}
}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}

View File

@ -84,7 +84,6 @@ public final class DirectoryInitialization
extractSysDirectory(context);
NativeLibrary.Initialize();
NativeLibrary.ReportStartToAnalytics();
areDirectoriesAvailable = true;

View File

@ -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,30 +90,41 @@ public final class StartupHandler
return null;
}
/**
* There isn't a good way to determine a new session. setSessionTime is called if the main
* activity goes into the background.
*/
public static void setSessionTime(Context context)
private static Instant getSessionTimestamp(Context context)
{
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
long timestamp = preferences.getLong(SESSION_TIMESTAMP, 0);
return Instant.ofEpochMilli(timestamp);
}
/**
* Called on activity stop / to set timestamp to "now".
*/
public static void updateSessionTimestamp(Activity activity)
{
Context context = activity.getApplicationContext();
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(Activity activity, 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)))
Context context = activity.getApplicationContext();
final Instant sessionTimestamp = getSessionTimestamp(context);
final Instant now = Instant.now();
if (firstStart || now.isAfter(sessionTimestamp.plus(6, ChronoUnit.HOURS)))
{
new AfterDirectoryInitializationRunner().runWithoutLifecycle(
// Just in case: ensure start event won't be accidentally sent too often.
updateSessionTimestamp(activity);
new AfterDirectoryInitializationRunner().runWithLifecycle((LifecycleOwner) activity,
NativeLibrary::ReportStartToAnalytics);
}
}