From 1eeded23df829973065d51c2595bf743910bc07e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 26 Dec 2022 19:26:58 +0100 Subject: [PATCH] Android: Add input profile management Co-authored-by: Charles Lombardo --- .../controlleremu/EmulatedController.java | 4 + .../features/input/ui/ProfileAdapter.java | 65 ++++++++ .../features/input/ui/ProfileDialog.kt | 76 +++++++++ .../input/ui/ProfileDialogPresenter.java | 150 ++++++++++++++++++ .../features/input/ui/ProfileViewHolder.java | 76 +++++++++ .../features/settings/ui/MenuTag.java | 15 +- .../settings/ui/SettingsActivity.java | 14 ++ .../settings/ui/SettingsActivityView.java | 16 ++ .../settings/ui/SettingsFragment.java | 14 ++ .../ui/SettingsFragmentPresenter.java | 15 +- .../settings/ui/SettingsFragmentView.java | 11 ++ .../utils/DirectoryInitialization.java | 14 +- .../app/src/main/res/drawable/ic_delete.xml | 9 ++ .../app/src/main/res/drawable/ic_save.xml | 9 ++ .../main/res/layout/dialog_input_profiles.xml | 25 +++ .../src/main/res/layout/list_item_profile.xml | 69 ++++++++ .../app/src/main/res/values/strings.xml | 8 + .../Android/jni/Input/EmulatedController.cpp | 28 ++++ 18 files changed, 612 insertions(+), 6 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.java create mode 100644 Source/Android/app/src/main/res/drawable/ic_delete.xml create mode 100644 Source/Android/app/src/main/res/drawable/ic_save.xml create mode 100644 Source/Android/app/src/main/res/layout/dialog_input_profiles.xml create mode 100644 Source/Android/app/src/main/res/layout/list_item_profile.xml diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.java index 5f6275ffcd..47d4dc5c96 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/controlleremu/EmulatedController.java @@ -35,6 +35,10 @@ public class EmulatedController public native void clearSettings(); + public native void loadProfile(String path); + + public native void saveProfile(String path); + public static native EmulatedController getGcPad(int controllerIndex); public static native EmulatedController getWiimote(int controllerIndex); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.java new file mode 100644 index 0000000000..9908d6f5fd --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileAdapter.java @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.input.ui; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding; + +public final class ProfileAdapter extends RecyclerView.Adapter +{ + private final Context mContext; + private final ProfileDialogPresenter mPresenter; + + private final String[] mStockProfileNames; + private final String[] mUserProfileNames; + + public ProfileAdapter(Context context, ProfileDialogPresenter presenter) + { + mContext = context; + mPresenter = presenter; + + mStockProfileNames = presenter.getProfileNames(true); + mUserProfileNames = presenter.getProfileNames(false); + } + + @NonNull @Override + public ProfileViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) + { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + + ListItemProfileBinding binding = ListItemProfileBinding.inflate(inflater, parent, false); + return new ProfileViewHolder(mPresenter, binding); + } + + @Override + public void onBindViewHolder(@NonNull ProfileViewHolder holder, int position) + { + if (position < mStockProfileNames.length) + { + holder.bind(mStockProfileNames[position], true); + return; + } + + position -= mStockProfileNames.length; + + if (position < mUserProfileNames.length) + { + holder.bind(mUserProfileNames[position], false); + return; + } + + holder.bindAsEmpty(mContext); + } + + @Override + public int getItemCount() + { + return mStockProfileNames.length + mUserProfileNames.length + 1; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt new file mode 100644 index 0000000000..2779ee7473 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialog.kt @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.input.ui + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.divider.MaterialDividerItemDecoration +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.databinding.DialogInputProfilesBinding +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag + +class ProfileDialog : BottomSheetDialogFragment() { + private var presenter: ProfileDialogPresenter? = null + + private var _binding: DialogInputProfilesBinding? = null + private val binding get() = _binding!! + + override fun onCreate(savedInstanceState: Bundle?) { + val menuTag: MenuTag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requireArguments().getSerializable(KEY_MENU_TAG, MenuTag::class.java) as MenuTag + } else { + requireArguments().getSerializable(KEY_MENU_TAG) as MenuTag + } + + presenter = ProfileDialogPresenter(this, menuTag) + + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogInputProfilesBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.profileList.adapter = ProfileAdapter(context, presenter) + binding.profileList.layoutManager = LinearLayoutManager(context) + val divider = MaterialDividerItemDecoration(requireActivity(), LinearLayoutManager.VERTICAL) + divider.isLastItemDecorated = false + binding.profileList.addItemDecoration(divider) + + // You can't expand a bottom sheet with a controller/remote/other non-touch devices + val behavior: BottomSheetBehavior = BottomSheetBehavior.from(view.parent as View) + if (!resources.getBoolean(R.bool.hasTouch)) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + private const val KEY_MENU_TAG = "menu_tag" + + @JvmStatic + fun create(menuTag: MenuTag): ProfileDialog { + val dialog = ProfileDialog() + val args = Bundle() + args.putSerializable(KEY_MENU_TAG, menuTag) + dialog.arguments = args + return dialog + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.java new file mode 100644 index 0000000000..ae35aec46d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileDialogPresenter.java @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.input.ui; + +import android.content.Context; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputEditText; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding; +import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; +import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivityView; +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; + +import java.io.File; +import java.text.Collator; +import java.util.Arrays; + +public final class ProfileDialogPresenter +{ + private static final String EXTENSION = ".ini"; + + private final Context mContext; + private final DialogFragment mDialog; + private final MenuTag mMenuTag; + + public ProfileDialogPresenter(DialogFragment dialog, MenuTag menuTag) + { + mContext = dialog.getContext(); + mDialog = dialog; + mMenuTag = menuTag; + } + + public String[] getProfileNames(boolean stock) + { + File[] profiles = new File(getProfileDirectoryPath(stock)).listFiles( + file -> !file.isDirectory() && file.getName().endsWith(EXTENSION)); + + if (profiles == null) + return new String[0]; + + return Arrays.stream(profiles) + .map(file -> file.getName().substring(0, file.getName().length() - EXTENSION.length())) + .sorted(Collator.getInstance()) + .toArray(String[]::new); + } + + public void loadProfile(@NonNull String profileName, boolean stock) + { + new MaterialAlertDialogBuilder(mContext) + .setMessage(mContext.getString(R.string.input_profile_confirm_load, profileName)) + .setPositiveButton(R.string.yes, (dialogInterface, i) -> + { + mMenuTag.getCorrespondingEmulatedController() + .loadProfile(getProfilePath(profileName, stock)); + ((SettingsActivityView) mDialog.requireActivity()).onControllerSettingsChanged(); + mDialog.dismiss(); + }) + .setNegativeButton(R.string.no, null) + .show(); + } + + public void saveProfile(@NonNull String profileName) + { + // If the user is saving over an existing profile, we should show an overwrite warning. + // If the user is creating a new profile, we normally shouldn't show a warning, + // but if they've entered the name of an existing profile, we should shown an overwrite warning. + + String profilePath = getProfilePath(profileName, false); + if (!new File(profilePath).exists()) + { + mMenuTag.getCorrespondingEmulatedController().saveProfile(profilePath); + mDialog.dismiss(); + } + else + { + new MaterialAlertDialogBuilder(mContext) + .setMessage(mContext.getString(R.string.input_profile_confirm_save, profileName)) + .setPositiveButton(R.string.yes, (dialogInterface, i) -> + { + mMenuTag.getCorrespondingEmulatedController().saveProfile(profilePath); + mDialog.dismiss(); + }) + .setNegativeButton(R.string.no, null) + .show(); + } + } + + public void saveProfileAndPromptForName() + { + LayoutInflater inflater = LayoutInflater.from(mContext); + + DialogInputStringBinding binding = DialogInputStringBinding.inflate(inflater); + TextInputEditText input = binding.input; + + new MaterialAlertDialogBuilder(mContext) + .setView(binding.getRoot()) + .setPositiveButton(R.string.ok, (dialogInterface, i) -> + saveProfile(input.getText().toString())) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + public void deleteProfile(@NonNull String profileName) + { + new MaterialAlertDialogBuilder(mContext) + .setMessage(mContext.getString(R.string.input_profile_confirm_delete, profileName)) + .setPositiveButton(R.string.yes, (dialogInterface, i) -> + { + new File(getProfilePath(profileName, false)).delete(); + mDialog.dismiss(); + }) + .setNegativeButton(R.string.no, null) + .show(); + } + + private String getProfileDirectoryName() + { + if (mMenuTag.isGCPadMenu()) + return "GCPad"; + else if (mMenuTag.isWiimoteMenu()) + return "Wiimote"; + else + throw new UnsupportedOperationException(); + } + + private String getProfileDirectoryPath(boolean stock) + { + if (stock) + { + return DirectoryInitialization.getSysDirectory() + "/Profiles/" + getProfileDirectoryName() + + '/'; + } + else + { + return DirectoryInitialization.getUserDirectory() + "/Config/Profiles/" + + getProfileDirectoryName() + '/'; + } + } + + private String getProfilePath(String profileName, boolean stock) + { + return getProfileDirectoryPath(stock) + profileName + EXTENSION; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.java new file mode 100644 index 0000000000..d4d40c0c65 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ProfileViewHolder.java @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.input.ui; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.databinding.ListItemProfileBinding; + +public class ProfileViewHolder extends RecyclerView.ViewHolder +{ + private final ProfileDialogPresenter mPresenter; + private final ListItemProfileBinding mBinding; + + private String mProfileName; + private boolean mStock; + + public ProfileViewHolder(@NonNull ProfileDialogPresenter presenter, + @NonNull ListItemProfileBinding binding) + { + super(binding.getRoot()); + + mPresenter = presenter; + mBinding = binding; + + binding.buttonLoad.setOnClickListener(view -> loadProfile()); + binding.buttonSave.setOnClickListener(view -> saveProfile()); + binding.buttonDelete.setOnClickListener(view -> deleteProfile()); + } + + public void bind(String profileName, boolean stock) + { + mProfileName = profileName; + mStock = stock; + + mBinding.textName.setText(profileName); + + mBinding.buttonLoad.setVisibility(View.VISIBLE); + mBinding.buttonSave.setVisibility(stock ? View.GONE : View.VISIBLE); + mBinding.buttonDelete.setVisibility(stock ? View.GONE : View.VISIBLE); + } + + public void bindAsEmpty(Context context) + { + mProfileName = null; + mStock = false; + + mBinding.textName.setText(context.getText(R.string.input_profile_new)); + + mBinding.buttonLoad.setVisibility(View.GONE); + mBinding.buttonSave.setVisibility(View.VISIBLE); + mBinding.buttonDelete.setVisibility(View.GONE); + } + + private void loadProfile() + { + mPresenter.loadProfile(mProfileName, mStock); + } + + private void saveProfile() + { + if (mProfileName == null) + mPresenter.saveProfileAndPromptForName(); + else + mPresenter.saveProfile(mProfileName); + } + + private void deleteProfile() + { + mPresenter.deleteProfile(mProfileName); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.java index f97a013074..a0a67dd838 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.java @@ -4,6 +4,8 @@ package org.dolphinemu.dolphinemu.features.settings.ui; import androidx.annotation.NonNull; +import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController; + public enum MenuTag { SETTINGS("settings"), @@ -88,6 +90,16 @@ public enum MenuTag return subType; } + public EmulatedController getCorrespondingEmulatedController() + { + if (isGCPadMenu()) + return EmulatedController.getGcPad(getSubType()); + else if (isWiimoteMenu()) + return EmulatedController.getWiimote(getSubType()); + else + throw new UnsupportedOperationException(); + } + public boolean isSerialPort1Menu() { return this == CONFIG_SERIALPORT1; @@ -143,7 +155,8 @@ public enum MenuTag { for (MenuTag menuTag : MenuTag.values()) { - if (menuTag.tag.equals(tag) && menuTag.subType == subtype) return menuTag; + if (menuTag.tag.equals(tag) && menuTag.subType == subtype) + return menuTag; } throw new IllegalArgumentException("You are asking for a menu that is not available or " + diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java index d4ec285681..bce24c1c46 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java @@ -19,6 +19,7 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; @@ -44,6 +45,7 @@ public final class SettingsActivity extends AppCompatActivity implements Setting private static final String ARG_IS_WII = "is_wii"; private static final String KEY_MAPPING_ALL_DEVICES = "all_devices"; private static final String FRAGMENT_TAG = "settings"; + private static final String FRAGMENT_DIALOG_TAG = "settings_dialog"; private SettingsActivityPresenter mPresenter; private AlertDialog dialog; @@ -191,6 +193,12 @@ public final class SettingsActivity extends AppCompatActivity implements Setting transaction.commit(); } + @Override + public void showDialogFragment(DialogFragment fragment) + { + fragment.show(getSupportFragmentManager(), FRAGMENT_DIALOG_TAG); + } + private boolean areSystemAnimationsEnabled() { float duration = Settings.Global.getFloat( @@ -314,6 +322,12 @@ public final class SettingsActivity extends AppCompatActivity implements Setting mPresenter.onSettingChanged(); } + @Override + public void onControllerSettingsChanged() + { + getFragment().onControllerSettingsChanged(); + } + @Override public void onMenuTagAction(@NonNull MenuTag menuTag, int value) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.java index 203b14ca4b..464d0ea165 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivityView.java @@ -5,6 +5,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; import org.dolphinemu.dolphinemu.features.settings.model.Settings; @@ -21,6 +22,13 @@ public interface SettingsActivityView */ void showSettingsFragment(MenuTag menuTag, Bundle extras, boolean addToStack, String gameId); + /** + * Shows a DialogFragment. + * + * Only one can be shown at a time. + */ + void showDialogFragment(DialogFragment fragment); + /** * Called by a contained Fragment to get access to the Setting HashMap * loaded from disk, so that each Fragment doesn't need to perform its own @@ -60,6 +68,14 @@ public interface SettingsActivityView */ void onSettingChanged(); + /** + * Refetches the values of all controller settings. + * + * To be used when loading an input profile or performing some other action that changes all + * controller settings at once. + */ + void onControllerSettingsChanged(); + /** * Called by a containing Fragment to tell the containing Activity that the user wants to open the * MenuTag associated with a setting. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.java index fcc27c4e5d..5f3202e63e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.java @@ -13,6 +13,7 @@ import androidx.annotation.Nullable; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -221,6 +222,12 @@ public final class SettingsFragment extends Fragment implements SettingsFragment mActivity.showSettingsFragment(menuKey, null, true, getArguments().getString(ARGUMENT_GAME_ID)); } + @Override + public void showDialogFragment(DialogFragment fragment) + { + mActivity.showDialogFragment(fragment); + } + @Override public void showToastMessage(String message) { @@ -239,6 +246,13 @@ public final class SettingsFragment extends Fragment implements SettingsFragment mActivity.onSettingChanged(); } + @Override + public void onControllerSettingsChanged() + { + mAdapter.notifyAllSettingsChanged(); + mPresenter.updateOldControllerSettingsWarningVisibility(); + } + @Override public void onMenuTagAction(@NonNull MenuTag menuTag, int value) { 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 6cce1361ec..3d33237439 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 @@ -24,6 +24,7 @@ import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedCont import org.dolphinemu.dolphinemu.features.input.model.controlleremu.NumericSetting; import org.dolphinemu.dolphinemu.features.input.model.view.InputDeviceSetting; import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting; +import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog; import org.dolphinemu.dolphinemu.features.settings.model.AbstractBooleanSetting; import org.dolphinemu.dolphinemu.features.settings.model.AbstractIntSetting; import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting; @@ -1223,6 +1224,9 @@ public final class SettingsFragmentPresenter sl.add(new RunRunnable(mContext, R.string.input_clear, R.string.input_clear_description, R.string.input_reset_warning, 0, true, () -> clearControllerSettings(controller))); + sl.add(new RunRunnable(mContext, R.string.input_profiles, 0, 0, 0, true, + () -> mView.showDialogFragment(ProfileDialog.create(mMenuTag)))); + updateOldControllerSettingsWarningVisibility(controller); } @@ -1293,6 +1297,11 @@ public final class SettingsFragmentPresenter } } + public void updateOldControllerSettingsWarningVisibility() + { + updateOldControllerSettingsWarningVisibility(mMenuTag.getCorrespondingEmulatedController()); + } + private void updateOldControllerSettingsWarningVisibility(EmulatedController controller) { String defaultDevice = controller.getDefaultDevice(); @@ -1306,15 +1315,13 @@ public final class SettingsFragmentPresenter private void loadDefaultControllerSettings(EmulatedController controller) { controller.loadDefaultSettings(); - mView.getAdapter().notifyAllSettingsChanged(); - updateOldControllerSettingsWarningVisibility(controller); + mView.onControllerSettingsChanged(); } private void clearControllerSettings(EmulatedController controller) { controller.clearSettings(); - mView.getAdapter().notifyAllSettingsChanged(); - updateOldControllerSettingsWarningVisibility(controller); + mView.onControllerSettingsChanged(); } private static int getLogVerbosityEntries() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.java index 53a3010962..809f26f7cc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.java @@ -3,6 +3,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui; import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import org.dolphinemu.dolphinemu.features.settings.model.Settings; @@ -55,6 +56,8 @@ public interface SettingsFragmentView */ void loadSubMenu(MenuTag menuKey); + void showDialogFragment(DialogFragment fragment); + /** * Tell the Fragment to tell the containing activity to display a toast message. * @@ -72,6 +75,14 @@ public interface SettingsFragmentView */ void onSettingChanged(); + /** + * Refetches the values of all controller settings. + * + * To be used when loading an input profile or performing some other action that changes all + * controller settings at once. + */ + void onControllerSettingsChanged(); + /** * Have the fragment tell the containing Activity that the user wants to open the MenuTag * associated with a setting. 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 cf85e12740..45f2960bd9 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 @@ -45,6 +45,7 @@ public final class DirectoryInitialization new MutableLiveData<>(DirectoryInitializationState.NOT_YET_INITIALIZED); private static volatile boolean areDirectoriesAvailable = false; private static String userPath; + private static String sysPath; private static boolean isUsingLegacyUserDirectory = false; public enum DirectoryInitializationState @@ -153,7 +154,8 @@ public final class DirectoryInitialization } // Let the native code know where the Sys directory is. - SetSysDirectory(sysDirectory.getPath()); + sysPath = sysDirectory.getPath(); + SetSysDirectory(sysPath); } private static void deleteDirectoryRecursively(@NonNull final File file) @@ -204,6 +206,16 @@ public final class DirectoryInitialization return userPath; } + public static String getSysDirectory() + { + if (!areDirectoriesAvailable) + { + throw new IllegalStateException( + "DirectoryInitialization must run before accessing the Sys directory!"); + } + return sysPath; + } + public static File getGameListCache(Context context) { return new File(context.getExternalCacheDir(), "gamelist.cache"); diff --git a/Source/Android/app/src/main/res/drawable/ic_delete.xml b/Source/Android/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000000..c841ba25f4 --- /dev/null +++ b/Source/Android/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/Source/Android/app/src/main/res/drawable/ic_save.xml b/Source/Android/app/src/main/res/drawable/ic_save.xml new file mode 100644 index 0000000000..0f449672ee --- /dev/null +++ b/Source/Android/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/Source/Android/app/src/main/res/layout/dialog_input_profiles.xml b/Source/Android/app/src/main/res/layout/dialog_input_profiles.xml new file mode 100644 index 0000000000..d6ba18d39b --- /dev/null +++ b/Source/Android/app/src/main/res/layout/dialog_input_profiles.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/Source/Android/app/src/main/res/layout/list_item_profile.xml b/Source/Android/app/src/main/res/layout/list_item_profile.xml new file mode 100644 index 0000000000..343f3d7796 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/list_item_profile.xml @@ -0,0 +1,69 @@ + + + + + +