Android: Implement vibrate-on-press and dualshock vibration

This commit is contained in:
Connor McLaughlin 2020-11-08 15:18:37 +10:00
parent cf2d9b86b0
commit c1c88eb41c
16 changed files with 616 additions and 497 deletions

View File

@ -6,6 +6,7 @@
#include "common/log.h"
#include "common/string.h"
#include "common/string_util.h"
#include "common/timer.h"
#include "common/timestamp.h"
#include "core/bios.h"
#include "core/cheats.h"
@ -39,6 +40,7 @@ static jmethodID s_EmulationActivity_method_reportMessage;
static jmethodID s_EmulationActivity_method_onEmulationStarted;
static jmethodID s_EmulationActivity_method_onEmulationStopped;
static jmethodID s_EmulationActivity_method_onGameTitleChanged;
static jmethodID s_EmulationActivity_method_setVibration;
static jclass s_PatchCode_class;
static jmethodID s_PatchCode_constructor;
@ -185,6 +187,8 @@ void AndroidHostInterface::LoadAndConvertSettings()
&g_settings.cpu_overclock_denominator);
g_settings.cpu_overclock_enable = (overclock_percent != 100);
g_settings.UpdateOverclockActive();
m_vibration_enabled = m_settings_interface.GetBoolValue("Controller1", "Vibration", false);
}
void AndroidHostInterface::UpdateInputMap()
@ -353,8 +357,13 @@ void AndroidHostInterface::EmulationThreadLoop()
// simulate the system if not paused
if (System::IsRunning())
{
System::RunFrame();
if (m_vibration_enabled)
UpdateVibration();
}
// rendering
{
DrawImGuiWindows();
@ -432,10 +441,21 @@ std::unique_ptr<AudioStream> AndroidHostInterface::CreateAudioStream(AudioBacken
return CommonHostInterface::CreateAudioStream(backend);
}
void AndroidHostInterface::OnSystemPaused(bool paused)
{
CommonHostInterface::OnSystemPaused(paused);
if (m_vibration_enabled)
SetVibration(false);
}
void AndroidHostInterface::OnSystemDestroyed()
{
CommonHostInterface::OnSystemDestroyed();
ClearOSDMessages();
if (m_vibration_enabled)
SetVibration(false);
}
void AndroidHostInterface::OnRunningGameChanged()
@ -612,6 +632,51 @@ bool AndroidHostInterface::ImportPatchCodesFromString(const std::string& str)
return true;
}
void AndroidHostInterface::SetVibration(bool enabled)
{
const u64 current_time = Common::Timer::GetValue();
if (Common::Timer::ConvertValueToSeconds(current_time - m_last_vibration_update_time) < 0.1f &&
m_last_vibration_state == enabled)
{
return;
}
m_last_vibration_state = enabled;
m_last_vibration_update_time = current_time;
JNIEnv* env = AndroidHelpers::GetJNIEnv();
if (m_emulation_activity_object) {
env->CallVoidMethod(m_emulation_activity_object, s_EmulationActivity_method_setVibration,
static_cast<jboolean>(enabled));
}
}
void AndroidHostInterface::UpdateVibration()
{
static constexpr float THRESHOLD = 0.5f;
bool vibration_state = false;
for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
{
Controller* controller = System::GetController(i);
if (!controller)
continue;
const u32 motors = controller->GetVibrationMotorCount();
for (u32 j = 0; j < motors; j++)
{
if (controller->GetVibrationMotorStrength(j) >= THRESHOLD)
{
vibration_state = true;
break;
}
}
}
SetVibration(vibration_state);
}
extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV);
@ -653,6 +718,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
env->GetMethodID(emulation_activity_class, "onEmulationStopped", "()V")) == nullptr ||
(s_EmulationActivity_method_onGameTitleChanged =
env->GetMethodID(emulation_activity_class, "onGameTitleChanged", "(Ljava/lang/String;)V")) == nullptr ||
(s_EmulationActivity_method_setVibration =
env->GetMethodID(emulation_activity_class, "setVibration", "(Z)V")) == nullptr ||
(s_PatchCode_constructor = env->GetMethodID(s_PatchCode_class, "<init>", "(ILjava/lang/String;Z)V")) == nullptr)
{
Log_ErrorPrint("AndroidHostInterface lookups failed");

View File

@ -65,6 +65,7 @@ protected:
void ReleaseHostDisplay() override;
std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
void OnSystemPaused(bool paused) override;
void OnSystemDestroyed() override;
void OnRunningGameChanged() override;
@ -77,6 +78,8 @@ private:
void DestroyImGuiContext();
void LoadAndConvertSettings();
void SetVibration(bool enabled);
void UpdateVibration();
jobject m_java_object = {};
jobject m_emulation_activity_object = {};
@ -92,6 +95,10 @@ private:
std::thread m_emulation_thread;
std::atomic_bool m_emulation_thread_stop_request{false};
u64 m_last_vibration_update_time = 0;
bool m_last_vibration_state = false;
bool m_vibration_enabled = false;
};
namespace AndroidHelpers {

View File

@ -3,6 +3,7 @@
package="com.github.stenzek.duckstation">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application

View File

@ -15,6 +15,7 @@ public class AndroidHostInterface {
private Context mContext;
static public native String getScmVersion();
static public native AndroidHostInterface create(Context context, String userDirectory);
public AndroidHostInterface(Context context) {
@ -71,7 +72,9 @@ public class AndroidHostInterface {
public native void setDisplayAlignment(int alignment);
public native PatchCode[] getPatchCodeList();
public native void setPatchCodeEnabled(int index, boolean enabled);
public native boolean importPatchCodesFromString(String str);
public native void addOSDMessage(String message, float duration);
@ -81,10 +84,13 @@ public class AndroidHostInterface {
public native String importBIOSImage(byte[] data);
public native boolean isFastForwardEnabled();
public native void setFastForwardEnabled(boolean enabled);
public native String[] getMediaPlaylistPaths();
public native int getMediaPlaylistIndex();
public native boolean setMediaPlaylistIndex(int index);
static {

View File

@ -8,6 +8,7 @@ import android.content.res.Configuration;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Vibrator;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.View;
@ -124,6 +125,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
private void doApplySettings() {
AndroidHostInterface.getInstance().applySettings();
updateRequestedOrientation();
updateControllers();
}
private void applySettings() {
@ -163,9 +165,6 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
doApplySettings();
}
if (AndroidHostInterface.getInstance().isEmulationThreadPaused())
AndroidHostInterface.getInstance().pauseEmulationThread(false);
return;
}
@ -323,8 +322,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
private void showMenu() {
if (getBooleanSetting("Main/PauseOnMenu", false) &&
!AndroidHostInterface.getInstance().isEmulationThreadPaused())
{
!AndroidHostInterface.getInstance().isEmulationThreadPaused()) {
AndroidHostInterface.getInstance().pauseEmulationThread(true);
}
@ -485,8 +483,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
private void showDiscChangeMenu() {
final String[] paths = AndroidHostInterface.getInstance().getMediaPlaylistPaths();
final int currentPath = AndroidHostInterface.getInstance().getMediaPlaylistIndex();
if (paths == null)
{
if (paths == null) {
onMenuClosed();
return;
}
@ -515,6 +512,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
final String controllerType = getStringSetting("Controller1/Type", "DigitalController");
final String viewType = getStringSetting("Controller1/TouchscreenControllerView", "digital");
final boolean autoHideTouchscreenController = getBooleanSetting("Controller1/AutoHideTouchscreenController", false);
final boolean hapticFeedback = getBooleanSetting("Controller1/HapticFeedback", false);
final boolean vibration = getBooleanSetting("Controller1/Vibration", false);
final FrameLayout activityLayout = findViewById(R.id.frameLayout);
Log.i("EmulationActivity", "Controller type: " + controllerType);
@ -526,14 +525,18 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
if (mTouchscreenController != null) {
activityLayout.removeView(mTouchscreenController);
mTouchscreenController = null;
mVibratorService = null;
}
} else {
if (mTouchscreenController == null) {
mTouchscreenController = new TouchscreenControllerView(this);
if (vibration)
mVibratorService = (Vibrator) getSystemService(VIBRATOR_SERVICE);
activityLayout.addView(mTouchscreenController);
}
mTouchscreenController.init(0, controllerType, viewType);
mTouchscreenController.init(0, controllerType, viewType, hapticFeedback);
}
}
@ -578,4 +581,21 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
mInputDeviceListener = null;
}
private Vibrator mVibratorService;
public void setVibration(boolean enabled) {
if (mVibratorService == null)
return;
runOnUiThread(() -> {
if (mVibratorService == null)
return;
if (enabled)
mVibratorService.vibrate(1000);
else
mVibratorService.cancel();
});
}
}

View File

@ -9,19 +9,14 @@ import android.net.Uri;
import android.os.Build;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.nio.charset.Charset;

View File

@ -73,7 +73,9 @@ public class GameListEntry {
return mTitle;
}
public String getFileTitle() { return mFileTitle; }
public String getFileTitle() {
return mFileTitle;
}
public String getModifiedTime() {
return mModifiedTime;

View File

@ -5,6 +5,7 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.View;
/**
@ -14,6 +15,7 @@ public class TouchscreenControllerButtonView extends View {
private Drawable mUnpressedDrawable;
private Drawable mPressedDrawable;
private boolean mPressed = false;
private boolean mHapticFeedback = false;
private int mControllerIndex = -1;
private int mButtonCode = -1;
@ -81,6 +83,10 @@ public class TouchscreenControllerButtonView extends View {
mPressed = pressed;
invalidate();
updateControllerState();
if (mHapticFeedback) {
performHapticFeedback(pressed ? HapticFeedbackConstants.VIRTUAL_KEY : HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
}
}
public void setButtonCode(int controllerIndex, int code) {
@ -88,6 +94,10 @@ public class TouchscreenControllerButtonView extends View {
mButtonCode = code;
}
public void setHapticFeedback(boolean enabled) {
mHapticFeedback = enabled;
}
private void updateControllerState() {
if (mButtonCode >= 0)
AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex, mButtonCode, mPressed);

View File

@ -20,6 +20,7 @@ public class TouchscreenControllerView extends FrameLayout {
private View mMainView;
private ArrayList<TouchscreenControllerButtonView> mButtonViews = new ArrayList<>();
private ArrayList<TouchscreenControllerAxisView> mAxisViews = new ArrayList<>();
private boolean mHapticFeedback;
public TouchscreenControllerView(Context context) {
super(context);
@ -33,9 +34,10 @@ public class TouchscreenControllerView extends FrameLayout {
super(context, attrs, defStyle);
}
public void init(int controllerIndex, String controllerType, String viewType) {
public void init(int controllerIndex, String controllerType, String viewType, boolean hapticFeedback) {
mControllerIndex = controllerIndex;
mControllerType = controllerType;
mHapticFeedback = hapticFeedback;
mButtonViews.clear();
mAxisViews.clear();
@ -99,6 +101,7 @@ public class TouchscreenControllerView extends FrameLayout {
if (code >= 0) {
buttonView.setButtonCode(mControllerIndex, code);
buttonView.setHapticFeedback(mHapticFeedback);
mButtonViews.add(buttonView);
} else {
Log.e("TouchscreenController", String.format("Unknown button name '%s' " +

View File

@ -14,8 +14,7 @@
~ limitations under the License.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
app:key="CPU/Overclock"
app:title="CPU Overclocking"
@ -80,7 +79,7 @@
app:title="Log To File"
app:defaultValue="false"
app:summary="Writes log messages to duckstation.log in your user directory. Only use for debugging as it slows down emulation."
app:iconSpaceReserved="false"/>
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="Logging/LogToDebug"
app:title="Log To Logcat"

View File

@ -45,7 +45,7 @@
app:entryValues="@array/settings_audio_backend_values"
app:defaultValue="OpenSLES"
app:useSimpleSummaryProvider="true"
app:iconSpaceReserved="false"/>
app:iconSpaceReserved="false" />
<ListPreference
app:key="Audio/BufferSize"
app:title="Audio Buffer Size"

View File

@ -14,8 +14,7 @@
~ limitations under the License.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
app:key="Controller1/Type"
@ -43,7 +42,20 @@
app:title="Auto-Hide Touchscreen Controller"
app:defaultValue="false"
app:summary="Hides the touchscreen controller when an external controller is detected."
app:iconSpaceReserved="false"/>
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="Controller1/HapticFeedback"
app:title="Vibrate On Press"
app:defaultValue="false"
app:summary="Enables a short vibration when a touchscreen button is pressed. Requires &quot;Vibrate on Touch&quot; to be enabled on your device."
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
app:key="Controller1/Vibration"
app:title="Enable Vibration"
app:defaultValue="false"
app:summary="Forwards rumble from the game to the phone's vibration motor."
app:iconSpaceReserved="false" />
<ListPreference
app:key="MemoryCards/Card1Type"

View File

@ -14,8 +14,7 @@
~ limitations under the License.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
app:key="Display/CropMode"

View File

@ -14,8 +14,7 @@
~ limitations under the License.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
app:key="CDROM/ReadSpeedup"
app:title="CD-ROM Read Speedup"

View File

@ -14,8 +14,7 @@
~ limitations under the License.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
app:key="Main/EmulationSpeed"