diff --git a/Source/Android/app/build.gradle b/Source/Android/app/build.gradle index 57715c02dc..b798bc08a9 100644 --- a/Source/Android/app/build.gradle +++ b/Source/Android/app/build.gradle @@ -25,7 +25,7 @@ android { // TODO If this is ever modified, change application_id in strings.xml applicationId "org.dolphinemu.dolphinemu" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode(getBuildVersionCode()) diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 9074dfb3da..15bd0ee7fc 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ + @@ -118,6 +120,12 @@ android:exported="false" android:theme="@style/DolphinBase" /> + + diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java new file mode 100644 index 0000000000..532dcc8962 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.activities; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; + +public class UserDataActivity extends AppCompatActivity implements View.OnClickListener +{ + public static void launch(Context context) + { + Intent launcher = new Intent(context, UserDataActivity.class); + context.startActivity(launcher); + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_user_data); + + TextView textType = findViewById(R.id.text_type); + TextView textPath = findViewById(R.id.text_path); + TextView textAndroid11 = findViewById(R.id.text_android_11); + Button buttonOpenSystemFileManager = findViewById(R.id.button_open_system_file_manager); + + textType.setText(DirectoryInitialization.isUsingLegacyUserDirectory() ? + R.string.user_data_old_location : R.string.user_data_new_location); + + textPath.setText(DirectoryInitialization.getUserDirectory()); + + boolean show_android_11_text = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + !DirectoryInitialization.isUsingLegacyUserDirectory(); + textAndroid11.setVisibility(show_android_11_text ? View.VISIBLE : View.GONE); + + boolean show_file_manager_button = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; + buttonOpenSystemFileManager.setVisibility(show_file_manager_button ? View.VISIBLE : View.GONE); + + buttonOpenSystemFileManager.setOnClickListener(this); + } + + @Override + public void onClick(View v) + { + try + { + startActivity(getFileManagerIntent()); + } + catch (ActivityNotFoundException e) + { + new AlertDialog.Builder(this, R.style.DolphinDialogBase) + .setMessage(R.string.user_data_open_system_file_manager_failed) + .setPositiveButton(R.string.ok, null) + .show(); + } + } + + private Intent getFileManagerIntent() + { + // Fragile, but some phones don't expose the system file manager in any better way + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName("com.android.documentsui", "com.android.documentsui.files.FilesActivity"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java index 753dddf522..a26be57754 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java @@ -44,6 +44,7 @@ import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHold import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.Log; +import org.dolphinemu.dolphinemu.utils.PermissionsHandler; import java.io.File; import java.io.IOException; @@ -300,7 +301,17 @@ public final class SettingsAdapter extends RecyclerView.Adapter dialog.dismiss()); + builder.show(); + } + else + { + FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS); + } } public void onFilePickerFileClick(SettingsItem item, int position) 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 dfbe629c30..fe58749387 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 @@ -10,6 +10,7 @@ import android.text.TextUtils; import org.dolphinemu.dolphinemu.DolphinApplication; import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.activities.UserDataActivity; import org.dolphinemu.dolphinemu.features.settings.model.AbstractIntSetting; import org.dolphinemu.dolphinemu.features.settings.model.AbstractStringSetting; import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting; @@ -256,6 +257,8 @@ public final class SettingsFragmentPresenter sl.add(new SubmenuSetting(mContext, R.string.advanced_submenu, MenuTag.CONFIG_ADVANCED)); sl.add(new SubmenuSetting(mContext, R.string.log_submenu, MenuTag.CONFIG_LOG)); sl.add(new SubmenuSetting(mContext, R.string.debug_submenu, MenuTag.DEBUG)); + sl.add(new RunRunnable(mContext, R.string.user_data_submenu, 0, 0, 0, + () -> UserDataActivity.launch(mContext))); } private void addGeneralSettings(ArrayList sl) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java index 46302965b5..f55486d51d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java @@ -72,7 +72,7 @@ public final class MainActivity extends AppCompatActivity if (savedInstanceState == null) StartupHandler.HandleInit(this); - if (PermissionsHandler.hasWriteAccess(this)) + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { new AfterDirectoryInitializationRunner() .run(this, false, this::setPlatformTabsAndStartGameFileCacheService); @@ -249,16 +249,14 @@ public final class MainActivity extends AppCompatActivity if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - DirectoryInitialization.start(this); - new AfterDirectoryInitializationRunner() - .run(this, false, this::setPlatformTabsAndStartGameFileCacheService); - } - else - { - Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_LONG).show(); + PermissionsHandler.setWritePermissionDenied(); } + + DirectoryInitialization.start(this); + new AfterDirectoryInitializationRunner() + .run(this, false, this::setPlatformTabsAndStartGameFileCacheService); } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index ac846d4c24..38b8fed094 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -30,6 +30,7 @@ import org.dolphinemu.dolphinemu.model.GameFile; import org.dolphinemu.dolphinemu.model.TvSettingsItem; import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.ui.platform.Platform; +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.PermissionsHandler; @@ -287,15 +288,13 @@ public final class TvMainActivity extends FragmentActivity if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - DirectoryInitialization.start(this); - GameFileCacheService.startLoad(this); - } - else - { - Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_LONG).show(); + PermissionsHandler.setWritePermissionDenied(); } + + DirectoryInitialization.start(this); + GameFileCacheService.startLoad(this); } } @@ -314,7 +313,7 @@ public final class TvMainActivity extends FragmentActivity ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); mGameRows.clear(); - if (PermissionsHandler.hasWriteAccess(this)) + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { GameFileCacheService.startLoad(this); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java index 5a7f33f42a..4b07636a64 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java @@ -62,7 +62,7 @@ public class AfterDirectoryInitializationRunner runnable.run(); } else if (abortOnFailure && - showErrorMessage(context, DirectoryInitialization.getDolphinDirectoriesState(context))) + showErrorMessage(context, DirectoryInitialization.getDolphinDirectoriesState())) { runFinishedCallback(); } @@ -115,10 +115,6 @@ public class AfterDirectoryInitializationRunner { switch (state) { - case EXTERNAL_STORAGE_PERMISSION_NEEDED: - Toast.makeText(context, R.string.write_permission_needed, Toast.LENGTH_LONG).show(); - return true; - case CANT_FIND_EXTERNAL_STORAGE: Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG).show(); return true; 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 0dc5432c54..48963052dc 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 @@ -13,6 +13,7 @@ import android.os.Environment; import android.preference.PreferenceManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.dolphinemu.dolphinemu.NativeLibrary; @@ -42,12 +43,12 @@ public final class DirectoryInitialization private static volatile boolean areDirectoriesAvailable = false; private static String userPath; private static AtomicBoolean isDolphinDirectoryInitializationRunning = new AtomicBoolean(false); + private static boolean isUsingLegacyUserDirectory = false; public enum DirectoryInitializationState { NOT_YET_INITIALIZED, DOLPHIN_DIRECTORIES_INITIALIZED, - EXTERNAL_STORAGE_PERMISSION_NEEDED, CANT_FIND_EXTERNAL_STORAGE } @@ -65,34 +66,27 @@ public final class DirectoryInitialization { if (directoryState != DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED) { - if (PermissionsHandler.hasWriteAccess(context)) + if (setDolphinUserDirectory(context)) { - if (setDolphinUserDirectory(context)) + initializeInternalStorage(context); + boolean wiimoteIniWritten = initializeExternalStorage(context); + NativeLibrary.Initialize(); + NativeLibrary.ReportStartToAnalytics(); + + areDirectoriesAvailable = true; + + if (wiimoteIniWritten) { - initializeInternalStorage(context); - boolean wiimoteIniWritten = initializeExternalStorage(context); - NativeLibrary.Initialize(); - NativeLibrary.ReportStartToAnalytics(); - - areDirectoriesAvailable = true; - - if (wiimoteIniWritten) - { - // This has to be done after calling NativeLibrary.Initialize(), - // as it relies on the config system - EmulationActivity.updateWiimoteNewIniPreferences(context); - } - - directoryState = DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED; - } - else - { - directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; + // This has to be done after calling NativeLibrary.Initialize(), + // as it relies on the config system + EmulationActivity.updateWiimoteNewIniPreferences(context); } + + directoryState = DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED; } else { - directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; + directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; } } @@ -100,16 +94,32 @@ public final class DirectoryInitialization sendBroadcastState(directoryState, context); } + @Nullable + private static File getLegacyUserDirectoryPath() + { + File externalPath = Environment.getExternalStorageDirectory(); + if (externalPath == null) + return null; + + return new File(externalPath, "dolphin-emu"); + } + private static boolean setDolphinUserDirectory(Context context) { if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) return false; - File externalPath = Environment.getExternalStorageDirectory(); - if (externalPath == null) + isUsingLegacyUserDirectory = + preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context); + + File path = isUsingLegacyUserDirectory ? + getLegacyUserDirectoryPath() : context.getExternalFilesDir(null); + + if (path == null) return false; - userPath = externalPath.getAbsolutePath() + "/dolphin-emu"; + userPath = path.getAbsolutePath(); + Log.debug("[DirectoryInitialization] User Dir: " + userPath); NativeLibrary.SetUserDirectory(userPath); @@ -207,7 +217,8 @@ public final class DirectoryInitialization public static boolean shouldStart(Context context) { return !isDolphinDirectoryInitializationRunning.get() && - getDolphinDirectoriesState(context) == DirectoryInitializationState.NOT_YET_INITIALIZED; + getDolphinDirectoriesState() == DirectoryInitializationState.NOT_YET_INITIALIZED && + !isWaitingForWriteAccess(context); } public static boolean areDolphinDirectoriesReady() @@ -215,17 +226,9 @@ public final class DirectoryInitialization return directoryState == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED; } - public static DirectoryInitializationState getDolphinDirectoriesState(Context context) + public static DirectoryInitializationState getDolphinDirectoriesState() { - if (directoryState == DirectoryInitializationState.NOT_YET_INITIALIZED && - !PermissionsHandler.hasWriteAccess(context)) - { - return DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; - } - else - { - return directoryState; - } + return directoryState; } public static String getUserDirectory() @@ -335,11 +338,6 @@ public final class DirectoryInitialization } } - public static boolean isExternalStorageLegacy() - { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || Environment.isExternalStorageLegacy(); - } - public static boolean preferOldFolderPicker(Context context) { // As of January 2021, ACTION_OPEN_DOCUMENT_TREE seems to be broken on the Nvidia Shield TV @@ -347,16 +345,65 @@ public final class DirectoryInitialization // for the time being - Android 11 hasn't been released for this device. We have an explicit // check for Android 11 below in hopes that Nvidia will fix this before releasing Android 11. // - // No Android TV device other than the Nvidia Shield TV is known to have an implementation - // of ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but "fortunately" - // for us, the Nvidia Shield TV is the only Android TV device in existence so far that can - // run Dolphin at all (due to the 64-bit requirement), so we can ignore this problem. + // No Android TV device other than the Nvidia Shield TV is known to have an implementation of + // ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but "fortunately", no + // Android TV device other than the Shield TV is known to be able to run Dolphin (either due to + // the 64-bit requirement or due to the GLES 3.0 requirement), so we can ignore this problem. // // All phones which are running a compatible version of Android support ACTION_OPEN_DOCUMENT and - // ACTION_OPEN_DOCUMENT_TREE, as this is required by the Android CTS (unlike with Android TV). + // ACTION_OPEN_DOCUMENT_TREE, as this is required by the mobile Android CTS (unlike Android TV). - return Build.VERSION.SDK_INT < Build.VERSION_CODES.R && isExternalStorageLegacy() && - TvUtil.isLeanback(context); + return Build.VERSION.SDK_INT < Build.VERSION_CODES.R && + PermissionsHandler.isExternalStorageLegacy() && TvUtil.isLeanback(context); + } + + private static boolean isExternalFilesDirEmpty(Context context) + { + File dir = context.getExternalFilesDir(null); + if (dir == null) + return false; // External storage not available + + File[] contents = dir.listFiles(); + return contents == null || contents.length == 0; + } + + private static boolean legacyUserDirectoryExists() + { + try + { + return getLegacyUserDirectoryPath().exists(); + } + catch (SecurityException e) + { + // Most likely we don't have permission to read external storage. + // Return true so that external storage permissions will be requested. + // + // Strangely, we don't seem to trigger this case in practice, even with no permissions... + // But this only makes things more convenient for users, so no harm done. + + return true; + } + } + + private static boolean preferLegacyUserDirectory(Context context) + { + return PermissionsHandler.isExternalStorageLegacy() && + !PermissionsHandler.isWritePermissionDenied() && + isExternalFilesDirEmpty(context) && legacyUserDirectoryExists(); + } + + public static boolean isUsingLegacyUserDirectory() + { + return isUsingLegacyUserDirectory; + } + + public static boolean isWaitingForWriteAccess(Context context) + { + // This first check is only for performance, not correctness + if (getDolphinDirectoriesState() != DirectoryInitializationState.NOT_YET_INITIALIZED) + return false; + + return preferLegacyUserDirectory(context) && !PermissionsHandler.hasWriteAccess(context); } private static native void CreateUserDirectories(); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java index 63a70870bf..a4c69281f2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java @@ -2,10 +2,10 @@ package org.dolphinemu.dolphinemu.utils; -import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; +import android.os.Environment; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; @@ -15,38 +15,41 @@ import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; public class PermissionsHandler { public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + private static boolean sWritePermissionDenied = false; - @TargetApi(Build.VERSION_CODES.M) - public static boolean checkWritePermission(final FragmentActivity activity) + public static void requestWritePermission(final FragmentActivity activity) { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) - { - return true; - } + return; - int hasWritePermission = ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE); - - if (hasWritePermission != PackageManager.PERMISSION_GRANTED) - { - // We only care about displaying the "Don't ask again" check and can ignore the result. - // Previous toasts already explained the rationale. - activity.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE); - activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, - REQUEST_CODE_WRITE_PERMISSION); - return false; - } - - return true; + activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_WRITE_PERMISSION); } public static boolean hasWriteAccess(Context context) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - { - int hasWritePermission = ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE); - return hasWritePermission == PackageManager.PERMISSION_GRANTED; - } + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return true; - return true; + if (!isExternalStorageLegacy()) + return false; + + int hasWritePermission = ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE); + return hasWritePermission == PackageManager.PERMISSION_GRANTED; + } + + public static boolean isExternalStorageLegacy() + { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || Environment.isExternalStorageLegacy(); + } + + public static void setWritePermissionDenied() + { + sWritePermissionDenied = true; + } + + public static boolean isWritePermissionDenied() + { + return sWritePermissionDenied; } } 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 a3ce3d2802..547d6934d1 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 @@ -22,8 +22,9 @@ public final class StartupHandler public static void HandleInit(FragmentActivity parent) { - // Ask the user to grant write permission if it's not already granted - PermissionsHandler.checkWritePermission(parent); + // Ask the user to grant write permission if relevant and not already granted + if (DirectoryInitialization.isWaitingForWriteAccess(parent)) + PermissionsHandler.requestWritePermission(parent); // Ask the user if he wants to enable analytics if we haven't yet. Analytics.checkAnalyticsInit(parent); diff --git a/Source/Android/app/src/main/res/layout/activity_user_data.xml b/Source/Android/app/src/main/res/layout/activity_user_data.xml new file mode 100644 index 0000000000..4c8d3bfe45 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/activity_user_data.xml @@ -0,0 +1,56 @@ + + + + + + + + + +