Android: Add controller rumble support

Android can be funky with controller vibration. Of the three controlers I have that contain a
vibrator(PS3, Xbox360, 2017 Shield controller), only the Xbox360 controller registered as having
a vibrator. So YYMV depending on the driver support of the device.
This commit is contained in:
zackhow 2018-10-01 19:36:00 -04:00
parent dd922660c9
commit 3499a416e7
12 changed files with 339 additions and 98 deletions

View File

@ -7,15 +7,11 @@
package org.dolphinemu.dolphinemu;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.view.Surface;
import org.dolphinemu.dolphinemu.activities.EmulationActivity;
import org.dolphinemu.dolphinemu.utils.Log;
import org.dolphinemu.dolphinemu.utils.Rumble;
import java.lang.ref.WeakReference;
@ -245,22 +241,7 @@ public final class NativeLibrary
return;
}
if (PreferenceManager.getDefaultSharedPreferences(emulationActivity)
.getBoolean("phoneRumble", true))
{
Vibrator vibrator = (Vibrator) emulationActivity.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator())
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
}
else
{
vibrator.vibrate(100);
}
}
}
Rumble.checkRumble(padID, state);
}
public static native String GetUserSetting(String gameID, String Section, String Key);

View File

@ -50,6 +50,7 @@ import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper;
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
import org.dolphinemu.dolphinemu.utils.Java_GCAdapter;
import org.dolphinemu.dolphinemu.utils.Java_WiimoteAdapter;
import org.dolphinemu.dolphinemu.utils.Rumble;
import org.dolphinemu.dolphinemu.utils.TvUtil;
import java.lang.annotation.Retention;
@ -277,6 +278,7 @@ public final class EmulationActivity extends AppCompatActivity
Java_GCAdapter.manager = (UsbManager) getSystemService(Context.USB_SERVICE);
Java_WiimoteAdapter.manager = (UsbManager) getSystemService(Context.USB_SERVICE);
Rumble.initRumble(this);
setContentView(R.layout.activity_emulation);
@ -361,6 +363,13 @@ public final class EmulationActivity extends AppCompatActivity
mPosition = savedInstanceState.getInt(EXTRA_GRID_POSITION);
}
@Override
protected void onStop()
{
super.onStop();
Rumble.clear();
}
@Override
public void onBackPressed()
{
@ -693,6 +702,7 @@ public final class EmulationActivity extends AppCompatActivity
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putBoolean("phoneRumble", state);
editor.apply();
Rumble.setPhoneVibrator(state, this);
}
@ -950,6 +960,11 @@ public final class EmulationActivity extends AppCompatActivity
.commit();
}
public boolean deviceHasTouchScreen()
{
return mDeviceHasTouchScreen;
}
public String getSelectedTitle()
{
return mSelectedTitle;

View File

@ -3,14 +3,18 @@ package org.dolphinemu.dolphinemu.dialogs;
import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.view.InputBindingSetting;
import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile;
import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper;
import org.dolphinemu.dolphinemu.utils.Log;
import org.dolphinemu.dolphinemu.utils.Rumble;
import org.dolphinemu.dolphinemu.utils.TvUtil;
import java.util.ArrayList;
@ -49,7 +53,8 @@ public final class MotionAlertDialog extends AlertDialog
case KeyEvent.ACTION_UP:
if (!ControllerMappingHelper.shouldKeyBeIgnored(event.getDevice(), keyCode))
{
saveKeyInput(event);
setting.onKeyInput(event);
dismiss();
}
// Even if we ignore the key, we still consume it. Thus return true regardless.
return true;
@ -63,14 +68,12 @@ public final class MotionAlertDialog extends AlertDialog
public boolean onKeyLongPress(int keyCode, KeyEvent event)
{
// Option to clear by long back is only needed on the TV interface
if (TvUtil.isLeanback(getContext()))
if (TvUtil.isLeanback(getContext()) && keyCode == KeyEvent.KEYCODE_BACK)
{
if (keyCode == KeyEvent.KEYCODE_BACK)
{
clearBinding();
setting.clearValue();
dismiss();
return true;
}
}
return super.onKeyLongPress(keyCode, event);
}
@ -162,69 +165,10 @@ public final class MotionAlertDialog extends AlertDialog
if (numMovedAxis == 1)
{
mWaitingForEvent = false;
saveMotionInput(input, lastMovedRange, lastMovedDir);
setting.onMotionInput(input, lastMovedRange, lastMovedDir);
dismiss();
}
}
return true;
}
/**
* Saves the provided key input setting both to the INI file (so native code can use it) and as
* an Android preference (so it persists correctly and is human-readable.)
*
* @param keyEvent KeyEvent of this key press.
*/
private void saveKeyInput(KeyEvent keyEvent)
{
InputDevice device = keyEvent.getDevice();
String bindStr = "Device '" + device.getDescriptor() + "'-Button " + keyEvent.getKeyCode();
String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
saveInput(bindStr, uiString);
}
/**
* Saves the provided motion input setting both to the INI file (so native code can use it) and as
* an Android preference (so it persists correctly and is human-readable.)
*
* @param device InputDevice from which the input event originated.
* @param motionRange MotionRange of the movement
* @param axisDir Either '-' or '+'
*/
private void saveMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
char axisDir)
{
String bindStr =
"Device '" + device.getDescriptor() + "'-Axis " + motionRange.getAxis() + axisDir;
String uiString = device.getName() + ": Axis " + motionRange.getAxis() + axisDir;
saveInput(bindStr, uiString);
}
/**
* Save the input string to settings and SharedPreferences, then dismiss this Dialog.
*/
private void saveInput(String bind, String ui)
{
setting.setValue(bind);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
SharedPreferences.Editor editor = preferences.edit();
editor.putString(setting.getKey(), ui);
editor.apply();
dismiss();
}
private void clearBinding()
{
setting.setValue("");
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
SharedPreferences.Editor editor = preferences.edit();
editor.remove(setting.getKey());
editor.apply();
dismiss();
}
}

View File

@ -1,9 +1,15 @@
package org.dolphinemu.dolphinemu.features.settings.model.view;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.InputDevice;
import android.view.KeyEvent;
import org.dolphinemu.dolphinemu.DolphinApplication;
import org.dolphinemu.dolphinemu.features.settings.model.Setting;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
public final class InputBindingSetting extends SettingsItem
public class InputBindingSetting extends SettingsItem
{
public InputBindingSetting(String key, String section, int titleId, Setting setting)
{
@ -21,6 +27,37 @@ public final class InputBindingSetting extends SettingsItem
return setting.getValue();
}
/**
* Saves the provided key input setting both to the INI file (so native code can use it) and as
* an Android preference (so it persists correctly and is human-readable.)
*
* @param keyEvent KeyEvent of this key press.
*/
public void onKeyInput(KeyEvent keyEvent)
{
InputDevice device = keyEvent.getDevice();
String bindStr = "Device '" + device.getDescriptor() + "'-Button " + keyEvent.getKeyCode();
String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
setValue(bindStr, uiString);
}
/**
* Saves the provided motion input setting both to the INI file (so native code can use it) and as
* an Android preference (so it persists correctly and is human-readable.)
*
* @param device InputDevice from which the input event originated.
* @param motionRange MotionRange of the movement
* @param axisDir Either '-' or '+'
*/
public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
char axisDir)
{
String bindStr =
"Device '" + device.getDescriptor() + "'-Axis " + motionRange.getAxis() + axisDir;
String uiString = device.getName() + ": Axis " + motionRange.getAxis() + axisDir;
setValue(bindStr, uiString);
}
/**
* Write a value to the backing string. If that string was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
@ -28,8 +65,15 @@ public final class InputBindingSetting extends SettingsItem
* @param bind The input that will be bound
* @return null if overwritten successfully; otherwise, a newly created StringSetting.
*/
public StringSetting setValue(String bind)
public StringSetting setValue(String bind, String ui)
{
SharedPreferences
preferences =
PreferenceManager.getDefaultSharedPreferences(DolphinApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
editor.putString(getKey(), ui);
editor.apply();
if (getSetting() == null)
{
StringSetting setting = new StringSetting(getKey(), getSection(), bind);
@ -44,6 +88,11 @@ public final class InputBindingSetting extends SettingsItem
}
}
public void clearValue()
{
setValue("", "");
}
@Override
public int getType()
{

View File

@ -0,0 +1,75 @@
package org.dolphinemu.dolphinemu.features.settings.model.view;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Vibrator;
import android.view.InputDevice;
import android.view.KeyEvent;
import org.dolphinemu.dolphinemu.DolphinApplication;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.Setting;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.utils.Rumble;
public class RumbleBindingSetting extends InputBindingSetting
{
public RumbleBindingSetting(String key, String section, int titleId, Setting setting)
{
super(key, section, titleId, setting);
}
@Override
public String getValue()
{
if (getSetting() == null)
{
return "";
}
StringSetting setting = (StringSetting) getSetting();
return setting.getValue();
}
/**
* Just need the device when saving rumble.
*/
@Override
public void onKeyInput(KeyEvent keyEvent)
{
saveRumble(keyEvent.getDevice());
}
/**
* Just need the device when saving rumble.
*/
@Override
public void onMotionInput(InputDevice device,
InputDevice.MotionRange motionRange,
char axisDir)
{
saveRumble(device);
}
private void saveRumble(InputDevice device)
{
Vibrator vibrator = device.getVibrator();
if (vibrator != null && vibrator.hasVibrator())
{
setValue(device.getDescriptor(), device.getName());
Rumble.doRumble(vibrator);
}
else
{
setValue("",
DolphinApplication.getAppContext().getString(R.string.rumble_not_found));
}
}
@Override
public int getType()
{
return TYPE_RUMBLE_BINDING;
}
}

View File

@ -19,6 +19,7 @@ public abstract class SettingsItem
public static final int TYPE_SUBMENU = 4;
public static final int TYPE_INPUT_BINDING = 5;
public static final int TYPE_STRING_SINGLE_CHOICE = 6;
public static final int TYPE_RUMBLE_BINDING = 7;
private String mKey;
private String mSection;

View File

@ -21,6 +21,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.CheckBoxSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.InputBindingSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.RumbleBindingSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem;
import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.SliderSetting;
@ -29,6 +30,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.CheckBoxSettingViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.RumbleBindingViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder;
@ -90,6 +92,10 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new InputBindingSettingViewHolder(view, this, mContext);
case SettingsItem.TYPE_RUMBLE_BINDING:
view = inflater.inflate(R.layout.list_item_setting, parent, false);
return new RumbleBindingViewHolder(view, this, mContext);
default:
Log.error("[SettingsAdapter] Invalid view type: " + viewType);
return null;
@ -216,19 +222,17 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
{
final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
dialog.setTitle(R.string.input_binding);
dialog.setMessage(String.format(mContext.getString(R.string.input_binding_description),
dialog.setMessage(String.format(mContext.getString(
item instanceof RumbleBindingSetting ?
R.string.input_rumble_description : R.string.input_binding_description),
mContext.getString(item.getNameId())));
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), this);
dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear),
(dialogInterface, i) ->
{
item.setValue("");
SharedPreferences sharedPreferences =
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(item.getKey());
editor.apply();
item.clearValue();
});
dialog.setOnDismissListener(dialog1 ->
{

View File

@ -14,6 +14,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.CheckBoxSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.HeaderSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.InputBindingSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.RumbleBindingSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem;
import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.SliderSetting;
@ -632,6 +633,8 @@ public final class SettingsFragmentPresenter
bindingsSection.getSetting(SettingsFile.KEY_GCBIND_DPAD_LEFT + gcPadNumber);
Setting bindDPadRight =
bindingsSection.getSetting(SettingsFile.KEY_GCBIND_DPAD_RIGHT + gcPadNumber);
Setting gcEmuRumble =
bindingsSection.getSetting(SettingsFile.KEY_EMU_RUMBLE + gcPadNumber);
sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_A + gcPadNumber,
@ -682,6 +685,11 @@ public final class SettingsFragmentPresenter
Settings.SECTION_BINDINGS, R.string.generic_left, bindDPadLeft));
sl.add(new InputBindingSetting(SettingsFile.KEY_GCBIND_DPAD_RIGHT + gcPadNumber,
Settings.SECTION_BINDINGS, R.string.generic_right, bindDPadRight));
sl.add(new HeaderSetting(null, null, R.string.emulation_control_rumble, 0));
sl.add(new RumbleBindingSetting(SettingsFile.KEY_EMU_RUMBLE + gcPadNumber,
Settings.SECTION_BINDINGS, R.string.emulation_control_rumble, gcEmuRumble));
}
else // Adapter
{
@ -761,6 +769,8 @@ public final class SettingsFragmentPresenter
bindingsSection.getSetting(SettingsFile.KEY_WIIBIND_DPAD_LEFT + wiimoteNumber);
Setting bindDPadRight =
bindingsSection.getSetting(SettingsFile.KEY_WIIBIND_DPAD_RIGHT + wiimoteNumber);
Setting wiiEmuRumble =
bindingsSection.getSetting(SettingsFile.KEY_EMU_RUMBLE + wiimoteNumber);
sl.add(new SingleChoiceSetting(SettingsFile.KEY_WIIMOTE_EXTENSION,
Settings.SECTION_WIIMOTE + (wiimoteNumber - 3), R.string.wiimote_extensions,
@ -843,6 +853,11 @@ public final class SettingsFragmentPresenter
Settings.SECTION_BINDINGS, R.string.generic_left, bindDPadLeft));
sl.add(new InputBindingSetting(SettingsFile.KEY_WIIBIND_DPAD_RIGHT + wiimoteNumber,
Settings.SECTION_BINDINGS, R.string.generic_right, bindDPadRight));
sl.add(new HeaderSetting(null, null, R.string.emulation_control_rumble, 0));
sl.add(new RumbleBindingSetting(SettingsFile.KEY_EMU_RUMBLE + wiimoteNumber,
Settings.SECTION_BINDINGS, R.string.emulation_control_rumble, wiiEmuRumble));
}
private void addExtensionTypeSettings(ArrayList<SettingsItem> sl, int wiimoteNumber,

View File

@ -0,0 +1,53 @@
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.TextView;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.view.RumbleBindingSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter;
public class RumbleBindingViewHolder extends SettingViewHolder
{
private RumbleBindingSetting mItem;
private TextView mTextSettingName;
private TextView mTextSettingDescription;
private Context mContext;
public RumbleBindingViewHolder(View itemView, SettingsAdapter adapter, Context context)
{
super(itemView, adapter);
mContext = context;
}
@Override
protected void findViews(View root)
{
mTextSettingName = (TextView) root.findViewById(R.id.text_setting_name);
mTextSettingDescription = (TextView) root.findViewById(R.id.text_setting_description);
}
@Override
public void bind(SettingsItem item)
{
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
mItem = (RumbleBindingSetting) item;
mTextSettingName.setText(item.getNameId());
mTextSettingDescription.setText(sharedPreferences.getString(mItem.getKey(), ""));
}
@Override
public void onClick(View clicked)
{
getAdapter().onInputBindingClick(mItem, getAdapterPosition());
}
}

View File

@ -115,6 +115,8 @@ public final class SettingsFile
public static final String KEY_GCADAPTER_RUMBLE = "AdapterRumble";
public static final String KEY_GCADAPTER_BONGOS = "SimulateKonga";
public static final String KEY_EMU_RUMBLE = "EmuRumble";
public static final String KEY_WIIMOTE_TYPE = "Source";
public static final String KEY_WIIMOTE_EXTENSION = "Extension";

View File

@ -0,0 +1,98 @@
package org.dolphinemu.dolphinemu.utils;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.util.SparseArray;
import android.view.InputDevice;
import org.dolphinemu.dolphinemu.activities.EmulationActivity;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile;
import java.util.HashMap;
public class Rumble
{
private static Vibrator phoneVibrator;
private static SparseArray<Vibrator> emuVibrators;
public static void initRumble(EmulationActivity activity)
{
if (activity.deviceHasTouchScreen() &&
PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean("phoneRumble", true))
{
setPhoneVibrator(true, activity);
}
emuVibrators = new SparseArray<>();
for (int i = 0; i < 8; i++)
{
StringSetting deviceName =
(StringSetting) activity.getSettings().getSection(Settings.SECTION_BINDINGS)
.getSetting(SettingsFile.KEY_EMU_RUMBLE + i);
if (deviceName != null && !deviceName.getValue().isEmpty())
{
for (int id : InputDevice.getDeviceIds())
{
InputDevice device = InputDevice.getDevice(id);
if (deviceName.getValue().equals(device.getDescriptor()))
{
Vibrator vib = device.getVibrator();
if (vib != null && vib.hasVibrator())
emuVibrators.put(i, vib);
}
}
}
}
}
public static void setPhoneVibrator(boolean set, EmulationActivity activity)
{
if (set)
{
Vibrator vib = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null && vib.hasVibrator())
phoneVibrator = vib;
}
else
{
phoneVibrator = null;
}
}
public static void clear()
{
phoneVibrator = null;
emuVibrators.clear();
}
public static void checkRumble(int padId, double state)
{
if (phoneVibrator != null)
doRumble(phoneVibrator);
if (emuVibrators.get(padId) != null)
doRumble(emuVibrators.get(padId));
}
public static void doRumble(Vibrator vib)
{
// Check again that it exists and can vibrate
if (vib != null && vib.hasVibrator())
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
vib.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
}
else
{
vib.vibrate(100);
}
}
}
}

View File

@ -40,6 +40,7 @@
<string name="input_binding">Input Binding</string>
<string name="input_binding_description">Press or move an input to bind it to %1$s.</string>
<string name="input_rumble_description">Press or move any input to set rumble.</string>
<!-- Generic buttons (Shared with lots of stuff) -->
<string name="generic_buttons">Buttons</string>
@ -291,6 +292,9 @@
<string name="header_wiimote_general">General</string>
<string name="header_controllers">Controllers</string>
<!-- Rumble -->
<string name="rumble_not_found">Device rumble not found</string>
<string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
<string name="load_settings">Loading Settings...</string>
<string name="emulation_change_disc">Change Disc</string>