diff --git a/Source/Android/app/build.gradle b/Source/Android/app/build.gradle index c2228a2aaf..83b14de907 100644 --- a/Source/Android/app/build.gradle +++ b/Source/Android/app/build.gradle @@ -88,6 +88,9 @@ dependencies { implementation "com.android.support:leanback-v17:$androidSupportVersion" implementation "com.android.support:support-tv-provider:$androidSupportVersion" + // For REST calls + implementation 'com.android.volley:volley:1.1.0' + // For showing the banner as a circle a-la Material Design Guidelines implementation 'de.hdodenhof:circleimageview:2.1.0' diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java index 22ee55cb70..3fd6e2192b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java @@ -1,17 +1,28 @@ package org.dolphinemu.dolphinemu; import android.app.Application; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import org.dolphinemu.dolphinemu.services.DirectoryInitializationService; import org.dolphinemu.dolphinemu.utils.PermissionsHandler; +import org.dolphinemu.dolphinemu.utils.VolleyUtil; public class DolphinApplication extends Application { + public static final String FIRST_OPEN = "FIRST_OPEN"; @Override public void onCreate() { super.onCreate(); + // Passed at emulation start to trigger first open event. + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + SharedPreferences.Editor sPrefsEditor = preferences.edit(); + sPrefsEditor.putBoolean(FIRST_OPEN, true); + sPrefsEditor.apply(); + + VolleyUtil.init(getApplicationContext()); System.loadLibrary("main"); if (PermissionsHandler.hasWriteAccess(getApplicationContext())) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java index c986857e75..922c041a7b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -322,7 +322,7 @@ public final class NativeLibrary /** * Begins emulation. */ - public static native void Run(String path); + public static native void Run(String path, boolean firstOpen); /** * Begins emulation from the specified savestate. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.java index 5d960a3b9f..599559030a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.java @@ -27,13 +27,15 @@ public class Settings public static final String SECTION_BINDINGS = "Android"; + public static final String SECTION_ANALYTICS = "Analytics"; + private String gameId; private static final Map> configFileSectionsMap = new HashMap<>(); static { - configFileSectionsMap.put(SettingsFile.FILE_NAME_DOLPHIN, Arrays.asList(SECTION_INI_CORE, SECTION_INI_INTERFACE, SECTION_BINDINGS)); + configFileSectionsMap.put(SettingsFile.FILE_NAME_DOLPHIN, Arrays.asList(SECTION_INI_CORE, SECTION_INI_INTERFACE, SECTION_BINDINGS, SECTION_ANALYTICS)); configFileSectionsMap.put(SettingsFile.FILE_NAME_GFX, Arrays.asList(SECTION_GFX_SETTINGS, SECTION_GFX_ENHANCEMENTS, SECTION_GFX_HACKS, SECTION_STEREOSCOPY)); configFileSectionsMap.put(SettingsFile.FILE_NAME_WIIMOTE, Arrays.asList(SECTION_WIIMOTE + 1, SECTION_WIIMOTE + 2, SECTION_WIIMOTE + 3, SECTION_WIIMOTE + 4)); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java index fbdaf8ad92..98d62ccfd1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java @@ -210,14 +210,17 @@ public final class SettingsFragmentPresenter Setting overclock = null; Setting speedLimit = null; Setting audioStretch = null; + Setting analytics = null; SettingSection coreSection = mSettings.getSection(Settings.SECTION_INI_CORE); + SettingSection analyticsSection = mSettings.getSection(Settings.SECTION_ANALYTICS); cpuCore = coreSection.getSetting(SettingsFile.KEY_CPU_CORE); dualCore = coreSection.getSetting(SettingsFile.KEY_DUAL_CORE); overclockEnable = coreSection.getSetting(SettingsFile.KEY_OVERCLOCK_ENABLE); overclock = coreSection.getSetting(SettingsFile.KEY_OVERCLOCK_PERCENT); speedLimit = coreSection.getSetting(SettingsFile.KEY_SPEED_LIMIT); audioStretch = coreSection.getSetting(SettingsFile.KEY_AUDIO_STRETCH); + analytics = analyticsSection.getSetting(SettingsFile.KEY_ANALYTICS_ENABLED); // TODO: Having different emuCoresEntries/emuCoresValues for each architecture is annoying. // The proper solution would be to have one emuCoresEntries and one emuCoresValues @@ -246,6 +249,7 @@ public final class SettingsFragmentPresenter sl.add(new SliderSetting(SettingsFile.KEY_OVERCLOCK_PERCENT, Settings.SECTION_INI_CORE, R.string.overclock_title, R.string.overclock_title_description, 400, "%", 100, overclock)); sl.add(new SliderSetting(SettingsFile.KEY_SPEED_LIMIT, Settings.SECTION_INI_CORE, R.string.speed_limit, 0, 200, "%", 100, speedLimit)); sl.add(new CheckBoxSetting(SettingsFile.KEY_AUDIO_STRETCH, Settings.SECTION_INI_CORE, R.string.audio_stretch, R.string.audio_stretch_description, false, audioStretch)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_ANALYTICS_ENABLED, Settings.SECTION_ANALYTICS, R.string.analytics, 0, false, analytics)); } private void addInterfaceSettings(ArrayList sl) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java index 2260b413f7..b68c879716 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java @@ -50,6 +50,9 @@ public final class SettingsFile public static final String KEY_SLOT_A_DEVICE = "SlotA"; public static final String KEY_SLOT_B_DEVICE = "SlotB"; + public static final String KEY_ANALYTICS_ENABLED = "Enabled"; + public static final String KEY_ANALYTICS_PERMISSION_ASKED = "PermissionAsked"; + public static final String KEY_USE_PANIC_HANDLERS = "UsePanicHandlers"; public static final String KEY_OSD_MESSAGES = "OnScreenDisplayMessages"; @@ -294,12 +297,14 @@ public final class SettingsFile catch (FileNotFoundException e) { Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage()); - view.onSettingsFileNotFound(); + if (view != null) + view.onSettingsFileNotFound(); } catch (IOException e) { Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath()+ e.getMessage()); - view.onSettingsFileNotFound(); + if (view != null) + view.onSettingsFileNotFound(); } finally { @@ -384,12 +389,14 @@ public final class SettingsFile catch (FileNotFoundException e) { Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage()); - view.showToastMessage("Error saving " + fileName + ".ini: " + e.getMessage()); + if (view != null) + view.showToastMessage("Error saving " + fileName + ".ini: " + e.getMessage()); } catch (UnsupportedEncodingException e) { Log.error("[SettingsFile] Bad encoding; please file a bug report: " + fileName + ".ini: " + e.getMessage()); - view.showToastMessage("Error saving " + fileName + ".ini: " + e.getMessage()); + if (view != null) + view.showToastMessage("Error saving " + fileName + ".ini: " + e.getMessage()); } finally { @@ -489,6 +496,23 @@ public final class SettingsFile sections.put(Settings.SECTION_INI_CORE, coreSection); } + public static void firstAnalyticsAdd(boolean enabled) + { + HashMap dolphinSections = readFile(SettingsFile.FILE_NAME_DOLPHIN, null); + SettingSection analyticsSection = dolphinSections.get(Settings.SECTION_ANALYTICS); + + Setting analyticsEnabled = new StringSetting(KEY_ANALYTICS_ENABLED, Settings.SECTION_ANALYTICS, enabled ? "True" : "False"); + Setting analyticsFirstAsk = new StringSetting(KEY_ANALYTICS_PERMISSION_ASKED, Settings.SECTION_ANALYTICS, "True"); + + analyticsSection.putSetting(analyticsFirstAsk); + analyticsSection.putSetting(analyticsEnabled); + + dolphinSections.put(Settings.SECTION_ANALYTICS, analyticsSection); + + TreeMap saveSection = new TreeMap<>(dolphinSections); + saveFile(SettingsFile.FILE_NAME_DOLPHIN, saveSection, null); + } + /** * For a line of text, determines what type of data is being represented, and returns * a Setting object containing this data. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java index c7e13a9999..15e5980acc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java @@ -16,6 +16,7 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.Toast; +import org.dolphinemu.dolphinemu.DolphinApplication; import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.activities.EmulationActivity; @@ -84,7 +85,12 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); String gamePath = getArguments().getString(KEY_GAMEPATH); - mEmulationState = new EmulationState(gamePath, getTemporaryStateFilePath()); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + boolean firstOpen = preferences.getBoolean(DolphinApplication.FIRST_OPEN, true); + SharedPreferences.Editor sPrefsEditor = preferences.edit(); + sPrefsEditor.putBoolean(DolphinApplication.FIRST_OPEN, false); + sPrefsEditor.apply(); + mEmulationState = new EmulationState(gamePath, getTemporaryStateFilePath(), firstOpen); } /** @@ -264,10 +270,12 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C private Surface mSurface; private boolean mRunWhenSurfaceIsValid; private boolean loadPreviousTemporaryState; + private boolean firstOpen; private final String temporaryStatePath; - EmulationState(String gamePath, String temporaryStatePath) + EmulationState(String gamePath, String temporaryStatePath, boolean firstOpen) { + this.firstOpen = firstOpen; mGamePath = gamePath; this.temporaryStatePath = temporaryStatePath; // Starting state is stopped. @@ -411,7 +419,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C else { Log.debug("[EmulationFragment] Starting emulation thread."); - NativeLibrary.Run(mGamePath); + NativeLibrary.Run(mGamePath, firstOpen); } }, "NativeEmulation"); mEmulationThread.start(); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Analytics.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Analytics.java new file mode 100644 index 0000000000..158e7c2055 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/Analytics.java @@ -0,0 +1,125 @@ +package org.dolphinemu.dolphinemu.utils; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Build; +import android.preference.PreferenceManager; +import android.support.v4.content.LocalBroadcastManager; + +import com.android.volley.Request; +import com.android.volley.toolbox.StringRequest; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.settings.model.Settings; +import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile; +import org.dolphinemu.dolphinemu.services.DirectoryInitializationService; + +public class Analytics +{ + private static DirectoryStateReceiver directoryStateReceiver; + + private static final String analyticsAsked = Settings.SECTION_ANALYTICS + "_" + SettingsFile.KEY_ANALYTICS_PERMISSION_ASKED; + private static final String analyticsEnabled = Settings.SECTION_ANALYTICS + "_" + SettingsFile.KEY_ANALYTICS_ENABLED; + + private static final String DEVICE_MANUFACTURER = "DEVICE_MANUFACTURER"; + private static final String DEVICE_OS = "DEVICE_OS"; + private static final String DEVICE_MODEL = "DEVICE_MODEL"; + private static final String DEVICE_TYPE = "DEVICE_TYPE"; + + private static String deviceType; + + public static void checkAnalyticsInit(Context context) + { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + if (!preferences.getBoolean(analyticsAsked, false)) + { + if (!DirectoryInitializationService.areDolphinDirectoriesReady()) + { + // Wait for directories to get initialized + IntentFilter statusIntentFilter = new IntentFilter( + DirectoryInitializationService.BROADCAST_ACTION); + + directoryStateReceiver = new DirectoryStateReceiver(directoryInitializationState -> + { + if (directoryInitializationState == DirectoryInitializationService.DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED) + { + LocalBroadcastManager.getInstance(context).unregisterReceiver(directoryStateReceiver); + directoryStateReceiver = null; + showMessage(context, preferences); + } + }); + // Registers the DirectoryStateReceiver and its intent filters + LocalBroadcastManager.getInstance(context).registerReceiver( + directoryStateReceiver, + statusIntentFilter); + } + else + { + showMessage(context, preferences); + } + } + // Get device type now since we have a context + deviceType = TvUtil.isLeanback(context) ? "android-tv" : "android-mobile"; + } + + private static void showMessage(Context context, SharedPreferences preferences) + { + // We asked, set to true regardless of answer + SharedPreferences.Editor sPrefsEditor = preferences.edit(); + sPrefsEditor.putBoolean(analyticsAsked, true); + sPrefsEditor.apply(); + + new AlertDialog.Builder(context) + .setTitle(context.getString(R.string.analytics)) + .setMessage(context.getString(R.string.analytics_desc)) + .setPositiveButton(R.string.yes, (dialogInterface, i) -> + { + sPrefsEditor.putBoolean(analyticsEnabled, true); + sPrefsEditor.apply(); + SettingsFile.firstAnalyticsAdd(true); + }) + .setNegativeButton(R.string.no, (dialogInterface, i) -> + { + sPrefsEditor.putBoolean(analyticsEnabled, false); + sPrefsEditor.apply(); + SettingsFile.firstAnalyticsAdd(false); + }) + .create() + .show(); + } + + public static void sendReport(String endpoint, byte[] data) + { + StringRequest request = new StringRequest(Request.Method.POST, endpoint, + null, error -> Log.debug("Send Report Failure code: " + + error.networkResponse.statusCode)) + { + @Override + public byte[] getBody() + { + return data; + } + }; + + VolleyUtil.getQueue().add(request); + } + + public static String getValue(String key) + { + switch (key) + { + case DEVICE_MODEL: + return Build.MODEL; + case DEVICE_MANUFACTURER: + return Build.MANUFACTURER; + case DEVICE_OS: + return String.valueOf(Build.VERSION.SDK_INT); + case DEVICE_TYPE: + return deviceType; + default: + return ""; + } + } +} 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 1be60cd21f..232de7f347 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 @@ -14,6 +14,9 @@ public final class StartupHandler // Ask the user to grant write permission if it's not already granted PermissionsHandler.checkWritePermission(parent); + // Ask the user if he wants to enable analytics if we haven't yet. + Analytics.checkAnalyticsInit(parent); + String start_file = ""; Bundle extras = parent.getIntent().getExtras(); if (extras != null) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/VolleyUtil.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/VolleyUtil.java new file mode 100644 index 0000000000..1e523bb45f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/VolleyUtil.java @@ -0,0 +1,22 @@ +package org.dolphinemu.dolphinemu.utils; + +import android.content.Context; + +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; + +public class VolleyUtil +{ + private static RequestQueue queue; + + public static void init(Context context) + { + if (queue == null) + queue = Volley.newRequestQueue(context); + } + + public static RequestQueue getQueue() + { + return queue; + } +} diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 1121ff0f64..362b90f7c8 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -135,6 +135,8 @@ Enable sound output through the speaker on a real Wiimote (DolphinBar required). Audio Stretching Stretches audio to reduce stuttering. Increases latency. + Enable usage statistics reporting + If authorized, Dolphin can collect data on its performance, feature usage, and configuration, as well as data on your system\'s hardware and operating system.\n\nNo private data is ever collected. This data helps us understand how people and emulated games use Dolphin and prioritize our efforts. It also helps us identify rare configurations that are causing bugs, performance and stability issues. This authorization can be revoked at any time through Dolphin\'s settings. Thanks to GameTDB.com for providing GameCube and Wii covers! diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 2c457d6c9d..42a5632139 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -20,6 +20,10 @@ static jmethodID s_game_file_constructor; static jclass s_game_file_cache_class; static jfieldID s_game_file_cache_pointer; +static jclass s_analytics_class; +static jmethodID s_send_analytics_report; +static jmethodID s_get_analytics_value; + namespace IDCache { JavaVM* GetJavaVM() @@ -37,6 +41,20 @@ jmethodID GetDisplayAlertMsg() return s_display_alert_msg; } +jclass GetAnalyticsClass() +{ + return s_analytics_class; +} + +jmethodID GetSendAnalyticsReport() +{ + return s_send_analytics_report; +} + +jmethodID GetAnalyticsValue() +{ + return s_get_analytics_value; +} jclass GetGameFileClass() { return s_game_file_class; @@ -91,6 +109,13 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) s_game_file_cache_class = reinterpret_cast(env->NewGlobalRef(game_file_cache_class)); s_game_file_cache_pointer = env->GetFieldID(game_file_cache_class, "mPointer", "J"); + const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics"); + s_analytics_class = reinterpret_cast(env->NewGlobalRef(analytics_class)); + s_send_analytics_report = + env->GetStaticMethodID(s_analytics_class, "sendReport", "(Ljava/lang/String;[B)V"); + s_get_analytics_value = env->GetStaticMethodID(s_analytics_class, "getValue", + "(Ljava/lang/String;)Ljava/lang/String;"); + return JNI_VERSION; } @@ -103,6 +128,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_native_library_class); env->DeleteGlobalRef(s_game_file_class); env->DeleteGlobalRef(s_game_file_cache_class); + env->DeleteGlobalRef(s_analytics_class); } #ifdef __cplusplus diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 584b0634ab..5b302353ce 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -8,11 +8,17 @@ namespace IDCache { +static constexpr jint JNI_VERSION = JNI_VERSION_1_6; + JavaVM* GetJavaVM(); jclass GetNativeLibraryClass(); jmethodID GetDisplayAlertMsg(); +jclass GetAnalyticsClass(); +jmethodID GetSendAnalyticsReport(); +jmethodID GetAnalyticsValue(); + jclass GetGameFileClass(); jfieldID GetGameFilePointer(); jmethodID GetGameFileConstructor(); diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 69f76bfe3d..c5df6bfa29 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -16,6 +16,7 @@ #include #include +#include "Common/AndroidAnalytics.h" #include "Common/CPUDetect.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" @@ -26,6 +27,7 @@ #include "Common/MsgHandler.h" #include "Common/Version.h" +#include "Core/Analytics.h" #include "Core/Boot/Boot.h" #include "Core/BootManager.h" #include "Core/ConfigLoaders/GameConfigLoader.h" @@ -156,6 +158,47 @@ static bool MsgAlert(const char* caption, const char* text, bool yes_no, MsgType return result != JNI_FALSE; } +static void ReportSend(std::string endpoint, std::string report) +{ + // Associate the current Thread with the Java VM. + JNIEnv* env; + IDCache::GetJavaVM()->AttachCurrentThread(&env, nullptr); + + jbyteArray output_array = env->NewByteArray(report.size()); + jbyte* output = env->GetByteArrayElements(output_array, nullptr); + memcpy(output, report.data(), report.size()); + env->ReleaseByteArrayElements(output_array, output, 0); + env->CallStaticVoidMethod(IDCache::GetAnalyticsClass(), IDCache::GetSendAnalyticsReport(), + ToJString(env, endpoint), output_array); + + IDCache::GetJavaVM()->DetachCurrentThread(); +} + +static std::string GetAnalyticValue(std::string key) +{ + // Associate the current Thread with the Java VM. + JNIEnv* env; + bool attached = false; + int getEnvStat = + IDCache::GetJavaVM()->GetEnv(reinterpret_cast(&env), IDCache::JNI_VERSION); + if (getEnvStat == JNI_EDETACHED) + { + IDCache::GetJavaVM()->AttachCurrentThread(&env, nullptr); + attached = true; + } + + jstring value = reinterpret_cast(env->CallStaticObjectMethod( + IDCache::GetAnalyticsClass(), IDCache::GetAnalyticsValue(), ToJString(env, key))); + + std::string stdvalue = GetJString(env, value); + + // Only detach the thread if it wasn't already attached + if (attached) + IDCache::GetJavaVM()->DetachCurrentThread(); + + return stdvalue; +} + #ifdef __cplusplus extern "C" { #endif @@ -216,8 +259,8 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetProfiling jboolean enable); JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_WriteProfileResults(JNIEnv* env, jobject obj); -JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2( - JNIEnv* env, jobject obj, jstring jFile); +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2Z( + JNIEnv* env, jobject obj, jstring jFile, jboolean jfirstOpen); JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( JNIEnv* env, jobject obj, jstring jFile, jstring jSavestate, jboolean jDeleteSavestate); @@ -500,8 +543,8 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RefreshWiimo WiimoteReal::Refresh(); } -static void Run(const std::string& path, std::optional savestate_path = {}, - bool delete_savestate = false) +static void Run(const std::string& path, bool first_open, + std::optional savestate_path = {}, bool delete_savestate = false) { __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Running : %s", path.c_str()); @@ -510,10 +553,17 @@ static void Run(const std::string& path, std::optional savestate_pa OSD::AddCallback(OSD::CallbackType::Shutdown, ButtonManager::Shutdown); RegisterMsgAlertHandler(&MsgAlert); + Common::AndroidSetReportHandler(&ReportSend); + DolphinAnalytics::AndroidSetGetValFunc(&GetAnalyticValue); std::unique_lock guard(s_host_identity_lock); UICommon::Init(); + if (first_open) + { + DolphinAnalytics::Instance()->ReportDolphinStart(GetAnalyticValue("DEVICE_TYPE")); + } + WiimoteReal::InitAdapterClass(); // No use running the loop when booting fails @@ -551,17 +601,17 @@ static void Run(const std::string& path, std::optional savestate_pa } } -JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2( - JNIEnv* env, jobject obj, jstring jFile) +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2Z( + JNIEnv* env, jobject obj, jstring jFile, jboolean jfirstOpen) { - Run(GetJString(env, jFile)); + Run(GetJString(env, jFile), jfirstOpen); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( JNIEnv* env, jobject obj, jstring jFile, jstring jSavestate, jboolean jDeleteSavestate) { - Run(GetJString(env, jFile), GetJString(env, jSavestate), jDeleteSavestate); + Run(GetJString(env, jFile), false, GetJString(env, jSavestate), jDeleteSavestate); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ChangeDisc(JNIEnv* env, diff --git a/Source/Core/Common/Analytics.cpp b/Source/Core/Common/Analytics.cpp index 57670fd1e7..ab25cecabd 100644 --- a/Source/Core/Common/Analytics.cpp +++ b/Source/Core/Common/Analytics.cpp @@ -190,5 +190,4 @@ void HttpAnalyticsBackend::Send(std::string report) if (m_http.IsValid()) m_http.Post(m_endpoint, report); } - } // namespace Common diff --git a/Source/Core/Common/Analytics.h b/Source/Core/Common/Analytics.h index 6015d1d6f3..4516334ae5 100644 --- a/Source/Core/Common/Analytics.h +++ b/Source/Core/Common/Analytics.h @@ -183,5 +183,4 @@ protected: std::string m_endpoint; HttpRequest m_http{std::chrono::seconds{5}}; }; - } // namespace Common diff --git a/Source/Core/Common/AndroidAnalytics.cpp b/Source/Core/Common/AndroidAnalytics.cpp new file mode 100644 index 0000000000..43912e5b1b --- /dev/null +++ b/Source/Core/Common/AndroidAnalytics.cpp @@ -0,0 +1,27 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include + +#include "Common/AndroidAnalytics.h" + +namespace Common +{ +std::function s_android_send_report; +void AndroidSetReportHandler(std::function func) +{ + s_android_send_report = std::move(func); +} +AndroidAnalyticsBackend::AndroidAnalyticsBackend(std::string passed_endpoint) + : m_endpoint{std::move(passed_endpoint)} +{ +} +AndroidAnalyticsBackend::~AndroidAnalyticsBackend() = default; + +void AndroidAnalyticsBackend::Send(std::string report) +{ + s_android_send_report(m_endpoint, report); +} +} // namespace Common diff --git a/Source/Core/Common/AndroidAnalytics.h b/Source/Core/Common/AndroidAnalytics.h new file mode 100644 index 0000000000..b96d67cb19 --- /dev/null +++ b/Source/Core/Common/AndroidAnalytics.h @@ -0,0 +1,26 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. +#pragma once + +#include +#include + +#include "Common/Analytics.h" + +namespace Common +{ +void AndroidSetReportHandler(std::function function); + +class AndroidAnalyticsBackend : public AnalyticsReportingBackend +{ +public: + explicit AndroidAnalyticsBackend(const std::string endpoint); + + ~AndroidAnalyticsBackend() override; + void Send(std::string report) override; + +private: + std::string m_endpoint; +}; +} // namespace Common diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index d7327a2a47..507e08662e 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -71,6 +71,7 @@ endif() if(ANDROID) target_sources(common PRIVATE + AndroidAnalytics.cpp Logging/ConsoleListenerDroid.cpp ) elseif(WIN32) diff --git a/Source/Core/Core/Analytics.cpp b/Source/Core/Core/Analytics.cpp index e6fdc9309a..43c703a64d 100644 --- a/Source/Core/Core/Analytics.cpp +++ b/Source/Core/Core/Analytics.cpp @@ -10,6 +10,9 @@ #include #elif defined(__APPLE__) #include +#elif defined(ANDROID) +#include +#include "Common/AndroidAnalytics.h" #endif #include "Common/Analytics.h" @@ -35,6 +38,14 @@ constexpr const char* ANALYTICS_ENDPOINT = "https://analytics.dolphin-emu.org/re std::mutex DolphinAnalytics::s_instance_mutex; std::shared_ptr DolphinAnalytics::s_instance; +#if defined(ANDROID) +static std::function s_get_val_func; +void DolphinAnalytics::AndroidSetGetValFunc(std::function func) +{ + s_get_val_func = std::move(func); +} +#endif + DolphinAnalytics::DolphinAnalytics() { ReloadConfig(); @@ -59,7 +70,11 @@ void DolphinAnalytics::ReloadConfig() std::unique_ptr new_backend; if (SConfig::GetInstance().m_analytics_enabled) { +#if defined(ANDROID) + new_backend = std::make_unique(ANALYTICS_ENDPOINT); +#else new_backend = std::make_unique(ANALYTICS_ENDPOINT); +#endif } m_reporter.SetBackend(std::move(new_backend)); @@ -152,6 +167,9 @@ void DolphinAnalytics::MakeBaseBuilder() } #elif defined(ANDROID) builder.AddData("os-type", "android"); + builder.AddData("android-manufacturer", s_get_val_func("DEVICE_MANUFACTURER")); + builder.AddData("android-model", s_get_val_func("DEVICE_MODEL")); + builder.AddData("android-version", s_get_val_func("DEVICE_OS")); #elif defined(__APPLE__) builder.AddData("os-type", "osx"); diff --git a/Source/Core/Core/Analytics.h b/Source/Core/Core/Analytics.h index d1900d4ae9..8c2415b92d 100644 --- a/Source/Core/Core/Analytics.h +++ b/Source/Core/Core/Analytics.h @@ -10,6 +10,9 @@ #include "Common/Analytics.h" +#if defined(ANDROID) +#include +#endif // Non generic part of the Dolphin Analytics framework. See Common/Analytics.h // for the main documentation. @@ -19,10 +22,13 @@ public: // Performs lazy-initialization of a singleton and returns the instance. static std::shared_ptr Instance(); +#if defined(ANDROID) + // Get value from java. + static void AndroidSetGetValFunc(std::function function); +#endif // Resets and recreates the analytics system in order to reload // configuration. void ReloadConfig(); - // Rotates the unique identifier used for this instance of Dolphin and saves // it into the configuration. void GenerateNewIdentity();