Android: Implement vibrate-on-press and dualshock vibration
This commit is contained in:
parent
cf2d9b86b0
commit
c1c88eb41c
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -73,7 +73,9 @@ public class GameListEntry {
|
|||
return mTitle;
|
||||
}
|
||||
|
||||
public String getFileTitle() { return mFileTitle; }
|
||||
public String getFileTitle() {
|
||||
return mFileTitle;
|
||||
}
|
||||
|
||||
public String getModifiedTime() {
|
||||
return mModifiedTime;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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' " +
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 "Vibrate on Touch" 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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue