diff --git a/app/src/androidTest/java/rs/ruffle/InputEvents.kt b/app/src/androidTest/java/rs/ruffle/InputEvents.kt new file mode 100644 index 00000000..032a17d8 --- /dev/null +++ b/app/src/androidTest/java/rs/ruffle/InputEvents.kt @@ -0,0 +1,155 @@ +package rs.ruffle + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Point +import android.graphics.Rect +import android.net.Uri +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import java.io.File +import kotlin.math.min +import kotlin.math.roundToInt +import org.hamcrest.CoreMatchers +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +private const val BASIC_SAMPLE_PACKAGE = "rs.ruffle" +private const val LAUNCH_TIMEOUT = 5000L +private const val SWF_WIDTH = 550.0 +private const val SWF_HEIGHT = 400.0 + +@RunWith(AndroidJUnit4::class) +class InputEvents { + private lateinit var device: UiDevice + private lateinit var traceOutput: File + private lateinit var swfFile: File + + @Before + fun startMainActivityFromHomeScreen() { + // Initialize UiDevice instance + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + // Start from the home screen + device.pressHome() + + // Wait for launcher + val launcherPackage: String = device.launcherPackageName + ViewMatchers.assertThat(launcherPackage, CoreMatchers.notNullValue()) + device.wait( + Until.hasObject(By.pkg(launcherPackage).depth(0)), + LAUNCH_TIMEOUT + ) + + // Launch the app + val context = ApplicationProvider.getApplicationContext() + traceOutput = File.createTempFile("trace", ".txt", context.cacheDir) + swfFile = File.createTempFile("movie", ".swf", context.cacheDir) + val resources = InstrumentationRegistry.getInstrumentation().context.resources + val inStream = resources.openRawResource( + rs.ruffle.test.R.raw.input_test + ) + val bytes = inStream.readBytes() + swfFile.writeBytes(bytes) + val intent = context.packageManager.getLaunchIntentForPackage( + BASIC_SAMPLE_PACKAGE + )?.apply { + component = ComponentName("rs.ruffle", "rs.ruffle.PlayerActivity") + data = Uri.fromFile(swfFile) + putExtra("traceOutput", traceOutput.absolutePath) + // Clear out any previous instances + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + context.startActivity(intent) + + // Wait for the app to appear + device.wait( + Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), + LAUNCH_TIMEOUT + ) + } + + @Test + fun clickEvents() { + device.waitForWindowUpdate(null, 1000) + ViewMatchers.assertThat(device, CoreMatchers.notNullValue()) + + val player = device.findObject(By.desc("Ruffle Player")) + + val red = screenToSwf(player.visibleBounds, Point(50, 50)) + val blue = screenToSwf(player.visibleBounds, Point(500, 350)) + device.click(red) + device.click(blue) + device.drag(red.x, red.y, blue.x, blue.y, 100) + device.waitForWindowUpdate(null, 500) + + val trace = traceOutput.readLines() + ViewMatchers.assertThat( + trace, + CoreMatchers.equalTo( + listOf( + "Test started!", + "red received mouseDown", + "red received mouseUp", + "red received click", + "blue received mouseDown", + "blue received mouseUp", + "blue received click", + "red received mouseDown", + "blue received mouseUp" + ) + ) + ) + } + + @Test + fun keyEvents() { + device.waitForWindowUpdate(null, 1000) + ViewMatchers.assertThat(device, CoreMatchers.notNullValue()) + + device.pressKeyCode(KeyEvent.KEYCODE_A) + device.pressKeyCode(KeyEvent.KEYCODE_B) + + device.waitForWindowUpdate(null, 500) + + val trace = traceOutput.readLines() + ViewMatchers.assertThat( + trace, + CoreMatchers.equalTo( + listOf( + "Test started!", + "keyDown: keyCode = 65, charCode = 97", + "keyUp: keyCode = 65, charCode = 97", + "keyDown: keyCode = 66, charCode = 98", + "keyUp: keyCode = 66, charCode = 98" + ) + ) + ) + } + + private fun screenToSwf(playerBounds: Rect, point: Point): Point { + val stretchX = playerBounds.width() / SWF_WIDTH + val stretchY = playerBounds.height() / SWF_HEIGHT + val scaleFactor = min(stretchX, stretchY) + val swfScreenWidth = SWF_WIDTH * scaleFactor + val swfScreenHeight = SWF_HEIGHT * scaleFactor + val swfOffsetX = (playerBounds.width() - swfScreenWidth) / 2 + val swfOffsetY = (playerBounds.height() - swfScreenHeight) / 2 + return Point( + (playerBounds.left + swfOffsetX + point.x * scaleFactor).roundToInt(), + (playerBounds.top + swfOffsetY + point.y * scaleFactor).roundToInt() + ) + } +} + +private fun UiDevice.click(point: Point) { + this.click(point.x, point.y) +} diff --git a/app/src/androidTest/res/raw/input_test.swf b/app/src/androidTest/res/raw/input_test.swf new file mode 100644 index 00000000..4526f3aa Binary files /dev/null and b/app/src/androidTest/res/raw/input_test.swf differ diff --git a/app/src/androidTest/res/raw/input_tests/Test.as b/app/src/androidTest/res/raw/input_tests/Test.as new file mode 100644 index 00000000..0f37d2d6 --- /dev/null +++ b/app/src/androidTest/res/raw/input_tests/Test.as @@ -0,0 +1,35 @@ +package { + import flash.display.MovieClip; + import flash.events.MouseEvent; + import flash.display.DisplayObject; + import flash.events.KeyboardEvent; + + public class Test extends MovieClip { + public function Test() { + trace("Test started!"); + + addKeyListeners("stage", stage); + addMouseListeners("red", red); + addMouseListeners("blue", blue); + } + + function addMouseListeners(name: String, clip: DisplayObject) { + var listener = function(event: MouseEvent) { + trace(name + " received " + event.type); + }; + clip.addEventListener(MouseEvent.MOUSE_DOWN, listener); + clip.addEventListener(MouseEvent.MOUSE_UP, listener); + //clip.addEventListener(MouseEvent.MOUSE_OVER, listener); + //clip.addEventListener(MouseEvent.MOUSE_OUT, listener); + clip.addEventListener(MouseEvent.CLICK, listener); + } + + function addKeyListeners(name: String, clip: DisplayObject) { + var listener = function(event: KeyboardEvent) { + trace(event.type + ": keyCode = " + event.keyCode + ", charCode = " + event.charCode); + }; + stage.addEventListener(KeyboardEvent.KEY_DOWN, listener); + stage.addEventListener(KeyboardEvent.KEY_UP, listener); + } + } +} diff --git a/app/src/androidTest/res/raw/input_tests/test.fla b/app/src/androidTest/res/raw/input_tests/test.fla new file mode 100644 index 00000000..b1635fc0 Binary files /dev/null and b/app/src/androidTest/res/raw/input_tests/test.fla differ diff --git a/app/src/androidTest/res/raw/input_tests/test.swf b/app/src/androidTest/res/raw/input_tests/test.swf new file mode 100644 index 00000000..4526f3aa Binary files /dev/null and b/app/src/androidTest/res/raw/input_tests/test.swf differ diff --git a/app/src/main/java/rs/ruffle/PlayerActivity.kt b/app/src/main/java/rs/ruffle/PlayerActivity.kt index 01a71a62..924621ae 100644 --- a/app/src/main/java/rs/ruffle/PlayerActivity.kt +++ b/app/src/main/java/rs/ruffle/PlayerActivity.kt @@ -141,6 +141,9 @@ class PlayerActivity : GameActivity() { layout.id = contentViewId setContentView(layout) mSurfaceView = InputEnabledSurfaceView(this) + + mSurfaceView.contentDescription = "Ruffle Player" + val placeholder = findViewById(R.id.placeholder) val pars = placeholder.layoutParams as ConstraintLayout.LayoutParams val parent = placeholder.parent as ViewGroup diff --git a/src/keycodes.rs b/src/keycodes.rs index 8c4e51a4..f03565b4 100644 --- a/src/keycodes.rs +++ b/src/keycodes.rs @@ -1,85 +1,85 @@ use android_activity::input::Keycode as Android; use ruffle_core::events::KeyCode as Ruffle; -pub fn android_keycode_to_ruffle(android: Android) -> Option { +pub fn android_keycode_to_ruffle(android: Android) -> Option<(Ruffle, Option)> { Some(match android { - Android::A => Ruffle::A, - Android::B => Ruffle::B, - Android::C => Ruffle::C, - Android::D => Ruffle::D, - Android::E => Ruffle::E, - Android::F => Ruffle::F, - Android::G => Ruffle::G, - Android::H => Ruffle::H, - Android::I => Ruffle::I, - Android::J => Ruffle::J, - Android::K => Ruffle::K, - Android::L => Ruffle::L, - Android::M => Ruffle::M, - Android::N => Ruffle::N, - Android::O => Ruffle::O, - Android::P => Ruffle::P, - Android::Q => Ruffle::Q, - Android::R => Ruffle::R, - Android::S => Ruffle::S, - Android::T => Ruffle::T, - Android::U => Ruffle::U, - Android::V => Ruffle::V, - Android::W => Ruffle::W, - Android::X => Ruffle::X, - Android::Y => Ruffle::Y, - Android::Z => Ruffle::Z, - Android::Comma => Ruffle::Comma, - Android::Period => Ruffle::Period, - Android::Tab => Ruffle::Tab, - Android::Space => Ruffle::Space, - Android::Enter => Ruffle::Return, - Android::Del => Ruffle::Delete, - Android::Grave => Ruffle::Grave, - Android::Minus => Ruffle::Minus, - Android::Equals => Ruffle::Equals, - Android::LeftBracket => Ruffle::LBracket, - Android::RightBracket => Ruffle::RBracket, - Android::Backslash => Ruffle::Backslash, - Android::Semicolon => Ruffle::Semicolon, - Android::Apostrophe => Ruffle::Apostrophe, - Android::Slash => Ruffle::Slash, - Android::Plus => Ruffle::Plus, - Android::PageUp => Ruffle::PgUp, - Android::PageDown => Ruffle::PgDown, - Android::Escape => Ruffle::Escape, - Android::ForwardDel => Ruffle::Delete, - Android::CapsLock => Ruffle::CapsLock, - Android::ScrollLock => Ruffle::ScrollLock, - Android::MoveHome => Ruffle::Home, - Android::MoveEnd => Ruffle::End, - Android::Insert => Ruffle::Insert, - Android::F1 => Ruffle::F1, - Android::F2 => Ruffle::F2, - Android::F3 => Ruffle::F3, - Android::F4 => Ruffle::F4, - Android::F5 => Ruffle::F5, - Android::F6 => Ruffle::F6, - Android::F7 => Ruffle::F7, - Android::F8 => Ruffle::F8, - Android::F9 => Ruffle::F9, - Android::F10 => Ruffle::F10, - Android::F11 => Ruffle::F11, - Android::F12 => Ruffle::F12, - Android::Numpad0 => Ruffle::Numpad0, - Android::Numpad1 => Ruffle::Numpad1, - Android::Numpad2 => Ruffle::Numpad2, - Android::Numpad3 => Ruffle::Numpad3, - Android::Numpad4 => Ruffle::Numpad4, - Android::Numpad5 => Ruffle::Numpad5, - Android::Numpad6 => Ruffle::Numpad6, - Android::Numpad7 => Ruffle::Numpad7, - Android::Numpad8 => Ruffle::Numpad8, - Android::Numpad9 => Ruffle::Numpad9, - Android::NumpadDivide => Ruffle::NumpadSlash, - Android::NumpadSubtract => Ruffle::NumpadMinus, - Android::NumpadDot => Ruffle::NumpadPeriod, - Android::NumpadEnter => Ruffle::NumpadEnter, + Android::A => (Ruffle::A, Some('a')), + Android::B => (Ruffle::B, Some('b')), + Android::C => (Ruffle::C, Some('c')), + Android::D => (Ruffle::D, Some('d')), + Android::E => (Ruffle::E, Some('e')), + Android::F => (Ruffle::F, Some('f')), + Android::G => (Ruffle::G, Some('g')), + Android::H => (Ruffle::H, Some('h')), + Android::I => (Ruffle::I, Some('i')), + Android::J => (Ruffle::J, Some('j')), + Android::K => (Ruffle::K, Some('k')), + Android::L => (Ruffle::L, Some('l')), + Android::M => (Ruffle::M, Some('m')), + Android::N => (Ruffle::N, Some('n')), + Android::O => (Ruffle::O, Some('o')), + Android::P => (Ruffle::P, Some('p')), + Android::Q => (Ruffle::Q, Some('q')), + Android::R => (Ruffle::R, Some('r')), + Android::S => (Ruffle::S, Some('s')), + Android::T => (Ruffle::T, Some('t')), + Android::U => (Ruffle::U, Some('u')), + Android::V => (Ruffle::V, Some('v')), + Android::W => (Ruffle::W, Some('w')), + Android::X => (Ruffle::X, Some('x')), + Android::Y => (Ruffle::Y, Some('y')), + Android::Z => (Ruffle::Z, Some('z')), + Android::Comma => (Ruffle::Comma, Some(',')), + Android::Period => (Ruffle::Period, Some('.')), + Android::Tab => (Ruffle::Tab, Some('\t')), + Android::Space => (Ruffle::Space, Some(' ')), + Android::Enter => (Ruffle::Return, Some(13 as char)), + Android::Del => (Ruffle::Backspace, Some(8 as char)), + Android::Grave => (Ruffle::Grave, Some('`')), + Android::Minus => (Ruffle::Minus, Some('-')), + Android::Equals => (Ruffle::Equals, Some('=')), + Android::LeftBracket => (Ruffle::LBracket, Some('(')), + Android::RightBracket => (Ruffle::RBracket, Some(')')), + Android::Backslash => (Ruffle::Backslash, Some('\\')), + Android::Semicolon => (Ruffle::Semicolon, Some(';')), + Android::Apostrophe => (Ruffle::Apostrophe, Some('\'')), + Android::Slash => (Ruffle::Slash, Some('/')), + Android::Plus => (Ruffle::Plus, Some('+')), + Android::PageUp => (Ruffle::PgUp, None), + Android::PageDown => (Ruffle::PgDown, None), + Android::Escape => (Ruffle::Escape, None), + Android::ForwardDel => (Ruffle::Delete, Some(127 as char)), + Android::CapsLock => (Ruffle::CapsLock, None), + Android::ScrollLock => (Ruffle::ScrollLock, None), + Android::MoveHome => (Ruffle::Home, None), + Android::MoveEnd => (Ruffle::End, None), + Android::Insert => (Ruffle::Insert, None), + Android::F1 => (Ruffle::F1, None), + Android::F2 => (Ruffle::F2, None), + Android::F3 => (Ruffle::F3, None), + Android::F4 => (Ruffle::F4, None), + Android::F5 => (Ruffle::F5, None), + Android::F6 => (Ruffle::F6, None), + Android::F7 => (Ruffle::F7, None), + Android::F8 => (Ruffle::F8, None), + Android::F9 => (Ruffle::F9, None), + Android::F10 => (Ruffle::F10, None), + Android::F11 => (Ruffle::F11, None), + Android::F12 => (Ruffle::F12, None), + Android::Numpad0 => (Ruffle::Numpad0, Some('0')), + Android::Numpad1 => (Ruffle::Numpad1, Some('1')), + Android::Numpad2 => (Ruffle::Numpad2, Some('2')), + Android::Numpad3 => (Ruffle::Numpad3, Some('3')), + Android::Numpad4 => (Ruffle::Numpad4, Some('4')), + Android::Numpad5 => (Ruffle::Numpad5, Some('5')), + Android::Numpad6 => (Ruffle::Numpad6, Some('6')), + Android::Numpad7 => (Ruffle::Numpad7, Some('7')), + Android::Numpad8 => (Ruffle::Numpad8, Some('8')), + Android::Numpad9 => (Ruffle::Numpad9, Some('9')), + Android::NumpadDivide => (Ruffle::NumpadSlash, Some('/')), + Android::NumpadSubtract => (Ruffle::NumpadMinus, Some('-')), + Android::NumpadDot => (Ruffle::NumpadPeriod, Some('.')), + Android::NumpadEnter => (Ruffle::NumpadEnter, Some(13 as char)), _ => return None, }) } diff --git a/src/lib.rs b/src/lib.rs index 72c79bb4..a57e7ea7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -325,20 +325,18 @@ fn run(app: AndroidApp) { } InputEvent::KeyEvent(event) => { if let Some(player) = playerbox.as_ref() { - let Some(key_code) = + let Some((key_code, key_char)) = android_keycode_to_ruffle(event.key_code()) else { return InputStatus::Unhandled; }; let ruffle_event = match event.action() { - KeyAction::Down => PlayerEvent::KeyDown { - key_code, - key_char: None, - }, - KeyAction::Up => PlayerEvent::KeyUp { - key_code, - key_char: None, - }, + KeyAction::Down => { + PlayerEvent::KeyDown { key_code, key_char } + } + KeyAction::Up => { + PlayerEvent::KeyUp { key_code, key_char } + } _ => return InputStatus::Unhandled, }; player