aboutsummaryrefslogtreecommitdiff
path: root/backends/platform/android/org
diff options
context:
space:
mode:
Diffstat (limited to 'backends/platform/android/org')
-rw-r--r--backends/platform/android/org/inodes/gus/scummvm/EditableSurfaceView.java59
-rw-r--r--backends/platform/android/org/inodes/gus/scummvm/Event.java330
-rw-r--r--backends/platform/android/org/inodes/gus/scummvm/PluginProvider.java52
-rw-r--r--backends/platform/android/org/inodes/gus/scummvm/ScummVM.java317
-rw-r--r--backends/platform/android/org/inodes/gus/scummvm/ScummVMActivity.java446
-rw-r--r--backends/platform/android/org/inodes/gus/scummvm/ScummVMApplication.java29
-rw-r--r--backends/platform/android/org/inodes/gus/scummvm/Unpacker.java370
7 files changed, 1603 insertions, 0 deletions
diff --git a/backends/platform/android/org/inodes/gus/scummvm/EditableSurfaceView.java b/backends/platform/android/org/inodes/gus/scummvm/EditableSurfaceView.java
new file mode 100644
index 0000000000..5b71d4a3a5
--- /dev/null
+++ b/backends/platform/android/org/inodes/gus/scummvm/EditableSurfaceView.java
@@ -0,0 +1,59 @@
+package org.inodes.gus.scummvm;
+
+import android.content.Context;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.view.SurfaceView;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+public class EditableSurfaceView extends SurfaceView {
+ public EditableSurfaceView(Context context) {
+ super(context);
+ }
+
+ public EditableSurfaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EditableSurfaceView(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onCheckIsTextEditor() {
+ return true;
+ }
+
+ private class MyInputConnection extends BaseInputConnection {
+ public MyInputConnection() {
+ super(EditableSurfaceView.this, false);
+ }
+
+ @Override
+ public boolean performEditorAction(int actionCode) {
+ if (actionCode == EditorInfo.IME_ACTION_DONE) {
+ InputMethodManager imm = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+ return super.performEditorAction(actionCode); // Sends enter key
+ }
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ outAttrs.initialCapsMode = 0;
+ outAttrs.initialSelEnd = outAttrs.initialSelStart = -1;
+ outAttrs.inputType = (InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_VARIATION_NORMAL |
+ InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
+ outAttrs.imeOptions = (EditorInfo.IME_ACTION_DONE |
+ EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+
+ return new MyInputConnection();
+ }
+}
diff --git a/backends/platform/android/org/inodes/gus/scummvm/Event.java b/backends/platform/android/org/inodes/gus/scummvm/Event.java
new file mode 100644
index 0000000000..f9c7aba93b
--- /dev/null
+++ b/backends/platform/android/org/inodes/gus/scummvm/Event.java
@@ -0,0 +1,330 @@
+package org.inodes.gus.scummvm;
+
+import android.view.KeyEvent;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Event {
+ // Common::EventType enum.
+ // Must be kept in sync with common/events.h
+ public final static int EVENT_INVALID = 0;
+ public final static int EVENT_KEYDOWN = 1;
+ public final static int EVENT_KEYUP = 2;
+ public final static int EVENT_MOUSEMOVE = 3;
+ public final static int EVENT_LBUTTONDOWN = 4;
+ public final static int EVENT_LBUTTONUP = 5;
+ public final static int EVENT_RBUTTONDOWN = 6;
+ public final static int EVENT_RBUTTONUP = 7;
+ public final static int EVENT_WHEELUP = 8;
+ public final static int EVENT_WHEELDOWN = 9;
+ public final static int EVENT_QUIT = 10;
+ public final static int EVENT_SCREEN_CHANGED = 11;
+ public final static int EVENT_PREDICTIVE_DIALOG = 12;
+ public final static int EVENT_MBUTTONDOWN = 13;
+ public final static int EVENT_MBUTTONUP = 14;
+ public final static int EVENT_MAINMENU = 15;
+ public final static int EVENT_RTL = 16;
+
+ // common/keyboard.h
+ public final static int ASCII_F1 = 315;
+ public final static int ASCII_F2 = 316;
+ public final static int ASCII_F3 = 317;
+ public final static int ASCII_F4 = 318;
+ public final static int ASCII_F5 = 319;
+ public final static int ASCII_F6 = 320;
+ public final static int ASCII_F7 = 321;
+ public final static int ASCII_F8 = 322;
+ public final static int ASCII_F9 = 323;
+ public final static int ASCII_F10 = 324;
+ public final static int ASCII_F11 = 325;
+ public final static int ASCII_F12 = 326;
+ public final static int KBD_CTRL = 1 << 0;
+ public final static int KBD_ALT = 1 << 1;
+ public final static int KBD_SHIFT = 1 << 2;
+
+ public final static int KEYCODE_INVALID = 0;
+ public final static int KEYCODE_BACKSPACE = 8;
+ public final static int KEYCODE_TAB = 9;
+ public final static int KEYCODE_CLEAR = 12;
+ public final static int KEYCODE_RETURN = 13;
+ public final static int KEYCODE_PAUSE = 19;
+ public final static int KEYCODE_ESCAPE = 27;
+ public final static int KEYCODE_SPACE = 32;
+ public final static int KEYCODE_EXCLAIM = 33;
+ public final static int KEYCODE_QUOTEDBL = 34;
+ public final static int KEYCODE_HASH = 35;
+ public final static int KEYCODE_DOLLAR = 36;
+ public final static int KEYCODE_AMPERSAND = 38;
+ public final static int KEYCODE_QUOTE = 39;
+ public final static int KEYCODE_LEFTPAREN = 40;
+ public final static int KEYCODE_RIGHTPAREN = 41;
+ public final static int KEYCODE_ASTERISK = 42;
+ public final static int KEYCODE_PLUS = 43;
+ public final static int KEYCODE_COMMA = 44;
+ public final static int KEYCODE_MINUS = 45;
+ public final static int KEYCODE_PERIOD = 46;
+ public final static int KEYCODE_SLASH = 47;
+ public final static int KEYCODE_0 = 48;
+ public final static int KEYCODE_1 = 49;
+ public final static int KEYCODE_2 = 50;
+ public final static int KEYCODE_3 = 51;
+ public final static int KEYCODE_4 = 52;
+ public final static int KEYCODE_5 = 53;
+ public final static int KEYCODE_6 = 54;
+ public final static int KEYCODE_7 = 55;
+ public final static int KEYCODE_8 = 56;
+ public final static int KEYCODE_9 = 57;
+ public final static int KEYCODE_COLON = 58;
+ public final static int KEYCODE_SEMICOLON = 59;
+ public final static int KEYCODE_LESS = 60;
+ public final static int KEYCODE_EQUALS = 61;
+ public final static int KEYCODE_GREATER = 62;
+ public final static int KEYCODE_QUESTION = 63;
+ public final static int KEYCODE_AT = 64;
+ public final static int KEYCODE_LEFTBRACKET = 91;
+ public final static int KEYCODE_BACKSLASH = 92;
+ public final static int KEYCODE_RIGHTBRACKET = 93;
+ public final static int KEYCODE_CARET = 94;
+ public final static int KEYCODE_UNDERSCORE = 95;
+ public final static int KEYCODE_BACKQUOTE = 96;
+ public final static int KEYCODE_a = 97;
+ public final static int KEYCODE_b = 98;
+ public final static int KEYCODE_c = 99;
+ public final static int KEYCODE_d = 100;
+ public final static int KEYCODE_e = 101;
+ public final static int KEYCODE_f = 102;
+ public final static int KEYCODE_g = 103;
+ public final static int KEYCODE_h = 104;
+ public final static int KEYCODE_i = 105;
+ public final static int KEYCODE_j = 106;
+ public final static int KEYCODE_k = 107;
+ public final static int KEYCODE_l = 108;
+ public final static int KEYCODE_m = 109;
+ public final static int KEYCODE_n = 110;
+ public final static int KEYCODE_o = 111;
+ public final static int KEYCODE_p = 112;
+ public final static int KEYCODE_q = 113;
+ public final static int KEYCODE_r = 114;
+ public final static int KEYCODE_s = 115;
+ public final static int KEYCODE_t = 116;
+ public final static int KEYCODE_u = 117;
+ public final static int KEYCODE_v = 118;
+ public final static int KEYCODE_w = 119;
+ public final static int KEYCODE_x = 120;
+ public final static int KEYCODE_y = 121;
+ public final static int KEYCODE_z = 122;
+ public final static int KEYCODE_DELETE = 127;
+ // Numeric keypad
+ public final static int KEYCODE_KP0 = 256;
+ public final static int KEYCODE_KP1 = 257;
+ public final static int KEYCODE_KP2 = 258;
+ public final static int KEYCODE_KP3 = 259;
+ public final static int KEYCODE_KP4 = 260;
+ public final static int KEYCODE_KP5 = 261;
+ public final static int KEYCODE_KP6 = 262;
+ public final static int KEYCODE_KP7 = 263;
+ public final static int KEYCODE_KP8 = 264;
+ public final static int KEYCODE_KP9 = 265;
+ public final static int KEYCODE_KP_PERIOD = 266;
+ public final static int KEYCODE_KP_DIVIDE = 267;
+ public final static int KEYCODE_KP_MULTIPLY = 268;
+ public final static int KEYCODE_KP_MINUS = 269;
+ public final static int KEYCODE_KP_PLUS = 270;
+ public final static int KEYCODE_KP_ENTER = 271;
+ public final static int KEYCODE_KP_EQUALS = 272;
+ // Arrows + Home/End pad
+ public final static int KEYCODE_UP = 273;
+ public final static int KEYCODE_DOWN = 274;
+ public final static int KEYCODE_RIGHT = 275;
+ public final static int KEYCODE_LEFT = 276;
+ public final static int KEYCODE_INSERT = 277;
+ public final static int KEYCODE_HOME = 278;
+ public final static int KEYCODE_END = 279;
+ public final static int KEYCODE_PAGEUP = 280;
+ public final static int KEYCODE_PAGEDOWN = 281;
+ // Function keys
+ public final static int KEYCODE_F1 = 282;
+ public final static int KEYCODE_F2 = 283;
+ public final static int KEYCODE_F3 = 284;
+ public final static int KEYCODE_F4 = 285;
+ public final static int KEYCODE_F5 = 286;
+ public final static int KEYCODE_F6 = 287;
+ public final static int KEYCODE_F7 = 288;
+ public final static int KEYCODE_F8 = 289;
+ public final static int KEYCODE_F9 = 290;
+ public final static int KEYCODE_F10 = 291;
+ public final static int KEYCODE_F11 = 292;
+ public final static int KEYCODE_F12 = 293;
+ public final static int KEYCODE_F13 = 294;
+ public final static int KEYCODE_F14 = 295;
+ public final static int KEYCODE_F15 = 296;
+ // Key state modifier keys
+ public final static int KEYCODE_NUMLOCK = 300;
+ public final static int KEYCODE_CAPSLOCK = 301;
+ public final static int KEYCODE_SCROLLOCK = 302;
+ public final static int KEYCODE_RSHIFT = 303;
+ public final static int KEYCODE_LSHIFT = 304;
+ public final static int KEYCODE_RCTRL = 305;
+ public final static int KEYCODE_LCTRL = 306;
+ public final static int KEYCODE_RALT = 307;
+ public final static int KEYCODE_LALT = 308;
+ public final static int KEYCODE_RMETA = 309;
+ public final static int KEYCODE_LMETA = 310;
+ public final static int KEYCODE_LSUPER = 311; // Left "Windows" key
+ public final static int KEYCODE_RSUPER = 312; // Right "Windows" key
+ public final static int KEYCODE_MODE = 313; // "Alt Gr" key
+ public final static int KEYCODE_COMPOSE = 314; // Multi-key compose key
+ // Miscellaneous function keys
+ public final static int KEYCODE_HELP = 315;
+ public final static int KEYCODE_PRINT = 316;
+ public final static int KEYCODE_SYSREQ = 317;
+ public final static int KEYCODE_BREAK = 318;
+ public final static int KEYCODE_MENU = 319;
+ public final static int KEYCODE_POWER = 320; // Power Macintosh power key
+ public final static int KEYCODE_EURO = 321; // Some european keyboards
+ public final static int KEYCODE_UNDO = 322; // Atari keyboard has Undo
+
+ // Android KeyEvent keycode -> ScummVM keycode
+ public final static Map<Integer, Integer> androidKeyMap;
+ static {
+ Map<Integer, Integer> map = new HashMap<Integer, Integer>();
+
+ map.put(KeyEvent.KEYCODE_DEL, KEYCODE_BACKSPACE);
+ map.put(KeyEvent.KEYCODE_TAB, KEYCODE_TAB);
+ map.put(KeyEvent.KEYCODE_CLEAR, KEYCODE_CLEAR);
+ map.put(KeyEvent.KEYCODE_ENTER, KEYCODE_RETURN);
+ //map.put(??, KEYCODE_PAUSE);
+ map.put(KeyEvent.KEYCODE_BACK, KEYCODE_ESCAPE);
+ map.put(KeyEvent.KEYCODE_SPACE, KEYCODE_SPACE);
+ //map.put(??, KEYCODE_EXCLAIM);
+ //map.put(??, KEYCODE_QUOTEDBL);
+ map.put(KeyEvent.KEYCODE_POUND, KEYCODE_HASH);
+ //map.put(??, KEYCODE_DOLLAR);
+ //map.put(??, KEYCODE_AMPERSAND);
+ map.put(KeyEvent.KEYCODE_APOSTROPHE, KEYCODE_QUOTE);
+ //map.put(??, KEYCODE_LEFTPAREN);
+ //map.put(??, KEYCODE_RIGHTPAREN);
+ //map.put(??, KEYCODE_ASTERISK);
+ map.put(KeyEvent.KEYCODE_PLUS, KEYCODE_PLUS);
+ map.put(KeyEvent.KEYCODE_COMMA, KEYCODE_COMMA);
+ map.put(KeyEvent.KEYCODE_MINUS, KEYCODE_MINUS);
+ map.put(KeyEvent.KEYCODE_PERIOD, KEYCODE_PERIOD);
+ map.put(KeyEvent.KEYCODE_SLASH, KEYCODE_SLASH);
+ map.put(KeyEvent.KEYCODE_0, KEYCODE_0);
+ map.put(KeyEvent.KEYCODE_1, KEYCODE_1);
+ map.put(KeyEvent.KEYCODE_2, KEYCODE_2);
+ map.put(KeyEvent.KEYCODE_3, KEYCODE_3);
+ map.put(KeyEvent.KEYCODE_4, KEYCODE_4);
+ map.put(KeyEvent.KEYCODE_5, KEYCODE_5);
+ map.put(KeyEvent.KEYCODE_6, KEYCODE_6);
+ map.put(KeyEvent.KEYCODE_7, KEYCODE_7);
+ map.put(KeyEvent.KEYCODE_8, KEYCODE_8);
+ map.put(KeyEvent.KEYCODE_9, KEYCODE_9);
+ //map.put(??, KEYCODE_COLON);
+ map.put(KeyEvent.KEYCODE_SEMICOLON, KEYCODE_SEMICOLON);
+ //map.put(??, KEYCODE_LESS);
+ map.put(KeyEvent.KEYCODE_EQUALS, KEYCODE_EQUALS);
+ //map.put(??, KEYCODE_GREATER);
+ //map.put(??, KEYCODE_QUESTION);
+ map.put(KeyEvent.KEYCODE_AT, KEYCODE_AT);
+ map.put(KeyEvent.KEYCODE_LEFT_BRACKET, KEYCODE_LEFTBRACKET);
+ map.put(KeyEvent.KEYCODE_BACKSLASH, KEYCODE_BACKSLASH);
+ map.put(KeyEvent.KEYCODE_RIGHT_BRACKET, KEYCODE_RIGHTBRACKET);
+ //map.put(??, KEYCODE_CARET);
+ //map.put(??, KEYCODE_UNDERSCORE);
+ //map.put(??, KEYCODE_BACKQUOTE);
+ map.put(KeyEvent.KEYCODE_A, KEYCODE_a);
+ map.put(KeyEvent.KEYCODE_B, KEYCODE_b);
+ map.put(KeyEvent.KEYCODE_C, KEYCODE_c);
+ map.put(KeyEvent.KEYCODE_D, KEYCODE_d);
+ map.put(KeyEvent.KEYCODE_E, KEYCODE_e);
+ map.put(KeyEvent.KEYCODE_F, KEYCODE_f);
+ map.put(KeyEvent.KEYCODE_G, KEYCODE_g);
+ map.put(KeyEvent.KEYCODE_H, KEYCODE_h);
+ map.put(KeyEvent.KEYCODE_I, KEYCODE_i);
+ map.put(KeyEvent.KEYCODE_J, KEYCODE_j);
+ map.put(KeyEvent.KEYCODE_K, KEYCODE_k);
+ map.put(KeyEvent.KEYCODE_L, KEYCODE_l);
+ map.put(KeyEvent.KEYCODE_M, KEYCODE_m);
+ map.put(KeyEvent.KEYCODE_N, KEYCODE_n);
+ map.put(KeyEvent.KEYCODE_O, KEYCODE_o);
+ map.put(KeyEvent.KEYCODE_P, KEYCODE_p);
+ map.put(KeyEvent.KEYCODE_Q, KEYCODE_q);
+ map.put(KeyEvent.KEYCODE_R, KEYCODE_r);
+ map.put(KeyEvent.KEYCODE_S, KEYCODE_s);
+ map.put(KeyEvent.KEYCODE_T, KEYCODE_t);
+ map.put(KeyEvent.KEYCODE_U, KEYCODE_u);
+ map.put(KeyEvent.KEYCODE_V, KEYCODE_v);
+ map.put(KeyEvent.KEYCODE_W, KEYCODE_w);
+ map.put(KeyEvent.KEYCODE_X, KEYCODE_x);
+ map.put(KeyEvent.KEYCODE_Y, KEYCODE_y);
+ map.put(KeyEvent.KEYCODE_Z, KEYCODE_z);
+ //map.put(KeyEvent.KEYCODE_DEL, KEYCODE_DELETE); use BACKSPACE instead
+ //map.put(??, KEYCODE_KP_*);
+ map.put(KeyEvent.KEYCODE_DPAD_UP, KEYCODE_UP);
+ map.put(KeyEvent.KEYCODE_DPAD_DOWN, KEYCODE_DOWN);
+ map.put(KeyEvent.KEYCODE_DPAD_RIGHT, KEYCODE_RIGHT);
+ map.put(KeyEvent.KEYCODE_DPAD_LEFT, KEYCODE_LEFT);
+ //map.put(??, KEYCODE_INSERT);
+ //map.put(??, KEYCODE_HOME);
+ //map.put(??, KEYCODE_END);
+ //map.put(??, KEYCODE_PAGEUP);
+ //map.put(??, KEYCODE_PAGEDOWN);
+ //map.put(??, KEYCODE_F{1-15});
+ map.put(KeyEvent.KEYCODE_NUM, KEYCODE_NUMLOCK);
+ //map.put(??, KEYCODE_CAPSLOCK);
+ //map.put(??, KEYCODE_SCROLLLOCK);
+ map.put(KeyEvent.KEYCODE_SHIFT_RIGHT, KEYCODE_RSHIFT);
+ map.put(KeyEvent.KEYCODE_SHIFT_LEFT, KEYCODE_LSHIFT);
+ //map.put(??, KEYCODE_RCTRL);
+ //map.put(??, KEYCODE_LCTRL);
+ map.put(KeyEvent.KEYCODE_ALT_RIGHT, KEYCODE_RALT);
+ map.put(KeyEvent.KEYCODE_ALT_LEFT, KEYCODE_LALT);
+ // ?? META, SUPER
+ // ?? MODE, COMPOSE
+ // ?? HELP, PRINT, SYSREQ, BREAK, EURO, UNDO
+ map.put(KeyEvent.KEYCODE_MENU, KEYCODE_MENU);
+ map.put(KeyEvent.KEYCODE_POWER, KEYCODE_POWER);
+
+ androidKeyMap = Collections.unmodifiableMap(map);
+ }
+
+ public int type;
+ public boolean synthetic;
+ public int kbd_keycode;
+ public int kbd_ascii;
+ public int kbd_flags;
+ public int mouse_x;
+ public int mouse_y;
+ public boolean mouse_relative; // Used for trackball events
+
+ public Event() {
+ type = EVENT_INVALID;
+ synthetic = false;
+ }
+
+ public Event(int type) {
+ this.type = type;
+ synthetic = false;
+ }
+
+ public static Event KeyboardEvent(int type, int keycode, int ascii,
+ int flags) {
+ Event e = new Event();
+ e.type = type;
+ e.kbd_keycode = keycode;
+ e.kbd_ascii = ascii;
+ e.kbd_flags = flags;
+ return e;
+ }
+
+ public static Event MouseEvent(int type, int x, int y) {
+ Event e = new Event();
+ e.type = type;
+ e.mouse_x = x;
+ e.mouse_y = y;
+ return e;
+ }
+}
diff --git a/backends/platform/android/org/inodes/gus/scummvm/PluginProvider.java b/backends/platform/android/org/inodes/gus/scummvm/PluginProvider.java
new file mode 100644
index 0000000000..b4035a296b
--- /dev/null
+++ b/backends/platform/android/org/inodes/gus/scummvm/PluginProvider.java
@@ -0,0 +1,52 @@
+package org.inodes.gus.scummvm;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class PluginProvider extends BroadcastReceiver {
+ public final static String META_UNPACK_LIB =
+ "org.inodes.gus.scummvm.meta.UNPACK_LIB";
+
+ public void onReceive(Context context, Intent intent) {
+ if (!intent.getAction().equals(ScummVMApplication.ACTION_PLUGIN_QUERY))
+ return;
+
+ Bundle extras = getResultExtras(true);
+
+ final ActivityInfo info;
+ try {
+ info = context.getPackageManager()
+ .getReceiverInfo(new ComponentName(context, this.getClass()),
+ PackageManager.GET_META_DATA);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(this.toString(), "Error finding my own info?", e);
+ return;
+ }
+
+ String mylib = info.metaData.getString(META_UNPACK_LIB);
+ if (mylib != null) {
+ ArrayList<String> all_libs =
+ extras.getStringArrayList(ScummVMApplication.EXTRA_UNPACK_LIBS);
+
+ all_libs.add(new Uri.Builder()
+ .scheme("plugin")
+ .authority(context.getPackageName())
+ .path(mylib)
+ .toString());
+
+ extras.putStringArrayList(ScummVMApplication.EXTRA_UNPACK_LIBS,
+ all_libs);
+ }
+
+ setResultExtras(extras);
+ }
+}
diff --git a/backends/platform/android/org/inodes/gus/scummvm/ScummVM.java b/backends/platform/android/org/inodes/gus/scummvm/ScummVM.java
new file mode 100644
index 0000000000..bc0c5ef408
--- /dev/null
+++ b/backends/platform/android/org/inodes/gus/scummvm/ScummVM.java
@@ -0,0 +1,317 @@
+package org.inodes.gus.scummvm;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+
+import java.io.File;
+import java.util.concurrent.Semaphore;
+
+
+// At least in Android 2.1, eglCreateWindowSurface() requires an
+// EGLNativeWindowSurface object, which is hidden deep in the bowels
+// of libui. Until EGL is properly exposed, it's probably safer to
+// use the Java versions of most EGL functions :(
+
+public class ScummVM implements SurfaceHolder.Callback {
+ private final static String LOG_TAG = "ScummVM.java";
+
+ private final int AUDIO_FRAME_SIZE = 2 * 2; // bytes. 16bit audio * stereo
+ public static class AudioSetupException extends Exception {}
+
+ private long nativeScummVM; // native code hangs itself here
+ boolean scummVMRunning = false;
+
+ private native void create(AssetManager am);
+
+ public ScummVM(Context context) {
+ create(context.getAssets()); // Init C++ code, set nativeScummVM
+ }
+
+ private native void nativeDestroy();
+
+ public synchronized void destroy() {
+ if (nativeScummVM != 0) {
+ nativeDestroy();
+ nativeScummVM = 0;
+ }
+ }
+ protected void finalize() {
+ destroy();
+ }
+
+ // Surface creation:
+ // GUI thread: create surface, release lock
+ // ScummVM thread: acquire lock (block), read surface
+ //
+ // Surface deletion:
+ // GUI thread: post event, acquire lock (block), return
+ // ScummVM thread: read event, free surface, release lock
+ //
+ // In other words, ScummVM thread does this:
+ // acquire lock
+ // setup surface
+ // when SCREEN_CHANGED arrives:
+ // destroy surface
+ // release lock
+ // back to acquire lock
+ static final int configSpec[] = {
+ EGL10.EGL_RED_SIZE, 5,
+ EGL10.EGL_GREEN_SIZE, 5,
+ EGL10.EGL_BLUE_SIZE, 5,
+ EGL10.EGL_DEPTH_SIZE, 0,
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
+ EGL10.EGL_NONE,
+ };
+ EGL10 egl;
+ EGLDisplay eglDisplay = EGL10.EGL_NO_DISPLAY;
+ EGLConfig eglConfig;
+ EGLContext eglContext = EGL10.EGL_NO_CONTEXT;
+ EGLSurface eglSurface = EGL10.EGL_NO_SURFACE;
+ Semaphore surfaceLock = new Semaphore(0, true);
+ SurfaceHolder nativeSurface;
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ nativeSurface = holder;
+ surfaceLock.release();
+ }
+
+ public void surfaceChanged(SurfaceHolder holder, int format,
+ int width, int height) {
+ // Disabled while I debug GL problems
+ //pushEvent(new Event(Event.EVENT_SCREEN_CHANGED));
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ pushEvent(new Event(Event.EVENT_SCREEN_CHANGED));
+ try {
+ surfaceLock.acquire();
+ } catch (InterruptedException e) {
+ Log.e(this.toString(),
+ "Interrupted while waiting for surface lock", e);
+ }
+ }
+
+ // Called by ScummVM thread (from initBackend)
+ private void createScummVMGLContext() {
+ egl = (EGL10)EGLContext.getEGL();
+ eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+ int[] version = new int[2];
+ egl.eglInitialize(eglDisplay, version);
+ int[] num_config = new int[1];
+ egl.eglChooseConfig(eglDisplay, configSpec, null, 0, num_config);
+
+ final int numConfigs = num_config[0];
+ if (numConfigs <= 0)
+ throw new IllegalArgumentException("No configs match configSpec");
+
+ EGLConfig[] configs = new EGLConfig[numConfigs];
+ egl.eglChooseConfig(eglDisplay, configSpec, configs, numConfigs,
+ num_config);
+ eglConfig = configs[0];
+
+ eglContext = egl.eglCreateContext(eglDisplay, eglConfig,
+ EGL10.EGL_NO_CONTEXT, null);
+ }
+
+ // Called by ScummVM thread
+ protected void setupScummVMSurface() {
+ try {
+ surfaceLock.acquire();
+ } catch (InterruptedException e) {
+ Log.e(this.toString(),
+ "Interrupted while waiting for surface lock", e);
+ return;
+ }
+ eglSurface = egl.eglCreateWindowSurface(eglDisplay, eglConfig,
+ nativeSurface, null);
+ egl.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
+ }
+
+ // Called by ScummVM thread
+ protected void destroyScummVMSurface() {
+ if (eglSurface != null) {
+ egl.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
+ egl.eglDestroySurface(eglDisplay, eglSurface);
+ eglSurface = EGL10.EGL_NO_SURFACE;
+ }
+
+ surfaceLock.release();
+ }
+
+ public void setSurface(SurfaceHolder holder) {
+ holder.addCallback(this);
+ }
+
+ // Set scummvm config options
+ final public native static void loadConfigFile(String path);
+ final public native static void setConfMan(String key, int value);
+ final public native static void setConfMan(String key, String value);
+
+ // Feed an event to ScummVM. Safe to call from other threads.
+ final public native void pushEvent(Event e);
+
+ final private native void audioMixCallback(byte[] buf);
+
+ // Runs the actual ScummVM program and returns when it does.
+ // This should not be called from multiple threads simultaneously...
+ final public native int scummVMMain(String[] argv);
+
+ // Callbacks from C++ peer instance
+ //protected GraphicsMode[] getSupportedGraphicsModes() {}
+ protected void displayMessageOnOSD(String msg) {}
+ protected void setWindowCaption(String caption) {}
+ protected void showVirtualKeyboard(boolean enable) {}
+ protected String[] getSysArchives() { return new String[0]; }
+ protected String[] getPluginDirectories() { return new String[0]; }
+ protected void initBackend() throws AudioSetupException {
+ createScummVMGLContext();
+ initAudio();
+ }
+
+ private static class AudioThread extends Thread {
+ final private int buf_size;
+ private boolean is_paused = false;
+ final private ScummVM scummvm;
+ final private AudioTrack audio_track;
+
+ AudioThread(ScummVM scummvm, AudioTrack audio_track, int buf_size) {
+ super("AudioThread");
+ this.scummvm = scummvm;
+ this.audio_track = audio_track;
+ this.buf_size = buf_size;
+ setPriority(Thread.MAX_PRIORITY);
+ setDaemon(true);
+ }
+
+ public void pauseAudio() {
+ synchronized (this) {
+ is_paused = true;
+ }
+ audio_track.pause();
+ }
+
+ public void resumeAudio() {
+ synchronized (this) {
+ is_paused = false;
+ notifyAll();
+ }
+ audio_track.play();
+ }
+
+ public void run() {
+ byte[] buf = new byte[buf_size];
+ audio_track.play();
+ int offset = 0;
+ try {
+ while (true) {
+ synchronized (this) {
+ while (is_paused)
+ wait();
+ }
+
+ if (offset == buf.length) {
+ // Grab new audio data
+ scummvm.audioMixCallback(buf);
+ offset = 0;
+ }
+ int len = buf.length - offset;
+ int ret = audio_track.write(buf, offset, len);
+ if (ret < 0) {
+ Log.w(LOG_TAG, String.format(
+ "AudioTrack.write(%dB) returned error %d",
+ buf.length, ret));
+ break;
+ } else if (ret != len) {
+ Log.w(LOG_TAG, String.format(
+ "Short audio write. Wrote %dB, not %dB",
+ ret, buf.length));
+ // Buffer is full, so yield cpu for a while
+ Thread.sleep(100);
+ }
+ offset += ret;
+ }
+ } catch (InterruptedException e) {
+ Log.e(this.toString(), "Audio thread interrupted", e);
+ }
+ }
+ }
+ private AudioThread audio_thread;
+
+ final public int audioSampleRate() {
+ return AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
+ }
+
+ private void initAudio() throws AudioSetupException {
+ int sample_rate = audioSampleRate();
+ int buf_size =
+ AudioTrack.getMinBufferSize(sample_rate,
+ AudioFormat.CHANNEL_CONFIGURATION_STEREO,
+ AudioFormat.ENCODING_PCM_16BIT);
+ if (buf_size < 0) {
+ int guess = AUDIO_FRAME_SIZE * sample_rate / 100; // 10ms of audio
+ Log.w(LOG_TAG, String.format(
+ "Unable to get min audio buffer size (error %d). Guessing %dB.",
+ buf_size, guess));
+ buf_size = guess;
+ }
+ Log.d(LOG_TAG, String.format("Using %dB buffer for %dHZ audio",
+ buf_size, sample_rate));
+ AudioTrack audio_track =
+ new AudioTrack(AudioManager.STREAM_MUSIC,
+ sample_rate,
+ AudioFormat.CHANNEL_CONFIGURATION_STEREO,
+ AudioFormat.ENCODING_PCM_16BIT,
+ buf_size,
+ AudioTrack.MODE_STREAM);
+ if (audio_track.getState() != AudioTrack.STATE_INITIALIZED) {
+ Log.e(LOG_TAG, "Error initialising Android audio system.");
+ throw new AudioSetupException();
+ }
+
+ audio_thread = new AudioThread(this, audio_track, buf_size);
+ audio_thread.start();
+ }
+
+ public void pause() {
+ audio_thread.pauseAudio();
+ // TODO: need to pause engine too
+ }
+
+ public void resume() {
+ // TODO: need to resume engine too
+ audio_thread.resumeAudio();
+ }
+
+ static {
+ // For grabbing with gdb...
+ final boolean sleep_for_debugger = false;
+ if (sleep_for_debugger) {
+ try {
+ Thread.sleep(20*1000);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ //System.loadLibrary("scummvm");
+ File cache_dir = ScummVMApplication.getLastCacheDir();
+ String libname = System.mapLibraryName("scummvm");
+ File libpath = new File(cache_dir, libname);
+ System.load(libpath.getPath());
+ }
+}
diff --git a/backends/platform/android/org/inodes/gus/scummvm/ScummVMActivity.java b/backends/platform/android/org/inodes/gus/scummvm/ScummVMActivity.java
new file mode 100644
index 0000000000..29e1eba3d3
--- /dev/null
+++ b/backends/platform/android/org/inodes/gus/scummvm/ScummVMActivity.java
@@ -0,0 +1,446 @@
+package org.inodes.gus.scummvm;
+
+import android.app.AlertDialog;
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.Toast;
+
+import java.io.IOException;
+
+public class ScummVMActivity extends Activity {
+ private boolean _do_right_click;
+ private boolean _last_click_was_right;
+
+ // game pixels to move per trackball/dpad event.
+ // FIXME: replace this with proper mouse acceleration
+ private final static int TRACKBALL_SCALE = 2;
+
+ private class MyScummVM extends ScummVM {
+ private boolean scummvmRunning = false;
+
+ public MyScummVM() {
+ super(ScummVMActivity.this);
+ }
+
+ @Override
+ protected void initBackend() throws ScummVM.AudioSetupException {
+ synchronized (this) {
+ scummvmRunning = true;
+ notifyAll();
+ }
+ super.initBackend();
+ }
+
+ public void waitUntilRunning() throws InterruptedException {
+ synchronized (this) {
+ while (!scummvmRunning)
+ wait();
+ }
+ }
+
+ @Override
+ protected void displayMessageOnOSD(String msg) {
+ Log.i(this.toString(), "OSD: " + msg);
+ Toast.makeText(ScummVMActivity.this, msg, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ protected void setWindowCaption(final String caption) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ setTitle(caption);
+ }
+ });
+ }
+
+ @Override
+ protected String[] getPluginDirectories() {
+ String[] dirs = new String[1];
+ dirs[0] = ScummVMApplication.getLastCacheDir().getPath();
+ return dirs;
+ }
+
+ @Override
+ protected void showVirtualKeyboard(final boolean enable) {
+ if (getResources().getConfiguration().keyboard ==
+ Configuration.KEYBOARD_NOKEYS) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ showKeyboard(enable);
+ }
+ });
+ }
+ }
+ }
+ private MyScummVM scummvm;
+ private Thread scummvm_thread;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ _do_right_click = false;
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ setContentView(R.layout.main);
+ takeKeyEvents(true);
+
+ // This is a common enough error that we should warn about it
+ // explicitly.
+ if (!Environment.getExternalStorageDirectory().canRead()) {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.no_sdcard_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.no_sdcard)
+ .setNegativeButton(R.string.quit,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ finish();
+ }
+ })
+ .show();
+ return;
+ }
+
+ SurfaceView main_surface = (SurfaceView)findViewById(R.id.main_surface);
+ main_surface.setOnTouchListener(new View.OnTouchListener() {
+ public boolean onTouch(View v, MotionEvent event) {
+ return onTouchEvent(event);
+ }
+ });
+ main_surface.setOnKeyListener(new View.OnKeyListener() {
+ public boolean onKey(View v, int code, KeyEvent ev) {
+ return onKeyDown(code, ev);
+ }
+ });
+ main_surface.requestFocus();
+
+ // Start ScummVM
+ scummvm = new MyScummVM();
+ scummvm_thread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ runScummVM();
+ } catch (Exception e) {
+ Log.e("ScummVM", "Fatal error in ScummVM thread", e);
+ new AlertDialog.Builder(ScummVMActivity.this)
+ .setTitle("Error")
+ .setMessage(e.toString())
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .show();
+ finish();
+ }
+ }
+ }, "ScummVM");
+ scummvm_thread.start();
+
+ // Block UI thread until ScummVM has started. In particular,
+ // this means that surface and event callbacks should be safe
+ // after this point.
+ try {
+ scummvm.waitUntilRunning();
+ } catch (InterruptedException e) {
+ Log.e(this.toString(),
+ "Interrupted while waiting for ScummVM.initBackend", e);
+ finish();
+ }
+
+ scummvm.setSurface(main_surface.getHolder());
+ }
+
+ // Runs in another thread
+ private void runScummVM() throws IOException {
+ getFilesDir().mkdirs();
+ String[] args = {
+ "ScummVM-lib",
+ "--config=" + getFileStreamPath("scummvmrc").getPath(),
+ "--path=" + Environment.getExternalStorageDirectory().getPath(),
+ "--gui-theme=scummmodern",
+ "--savepath=" + getDir("saves", 0).getPath(),
+ };
+
+ int ret = scummvm.scummVMMain(args);
+
+ // On exit, tear everything down for a fresh
+ // restart next time.
+ System.exit(ret);
+ }
+
+ private boolean was_paused = false;
+
+ @Override
+ public void onPause() {
+ if (scummvm != null) {
+ was_paused = true;
+ scummvm.pause();
+ }
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (scummvm != null && was_paused)
+ scummvm.resume();
+ was_paused = false;
+ }
+
+ @Override
+ public void onStop() {
+ if (scummvm != null) {
+ scummvm.pushEvent(new Event(Event.EVENT_QUIT));
+ try {
+ scummvm_thread.join(1000); // 1s timeout
+ } catch (InterruptedException e) {
+ Log.i(this.toString(),
+ "Error while joining ScummVM thread", e);
+ }
+ }
+ super.onStop();
+ }
+
+ static final int MSG_MENU_LONG_PRESS = 1;
+ private final Handler keycodeMenuTimeoutHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_MENU_LONG_PRESS) {
+ InputMethodManager imm = (InputMethodManager)
+ getSystemService(INPUT_METHOD_SERVICE);
+ if (imm != null)
+ imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
+ }
+ }
+ };
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent kevent) {
+ return onKeyDown(keyCode, kevent);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount,
+ KeyEvent kevent) {
+ return onKeyDown(keyCode, kevent);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent kevent) {
+ // Filter out "special" keys
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MENU:
+ // Have to reimplement hold-down-menu-brings-up-softkeybd
+ // ourselves, since we are otherwise hijacking the menu
+ // key :(
+ // See com.android.internal.policy.impl.PhoneWindow.onKeyDownPanel()
+ // for the usual Android implementation of this feature.
+ if (kevent.getRepeatCount() > 0)
+ // Ignore keyrepeat for menu
+ return false;
+ boolean timeout_fired = false;
+ if (getResources().getConfiguration().keyboard ==
+ Configuration.KEYBOARD_NOKEYS) {
+ timeout_fired = !keycodeMenuTimeoutHandler.hasMessages(MSG_MENU_LONG_PRESS);
+ keycodeMenuTimeoutHandler.removeMessages(MSG_MENU_LONG_PRESS);
+ if (kevent.getAction() == KeyEvent.ACTION_DOWN) {
+ keycodeMenuTimeoutHandler.sendMessageDelayed(
+ keycodeMenuTimeoutHandler.obtainMessage(MSG_MENU_LONG_PRESS),
+ ViewConfiguration.getLongPressTimeout());
+ return true;
+ }
+ }
+ if (kevent.getAction() == KeyEvent.ACTION_UP) {
+ if (!timeout_fired)
+ scummvm.pushEvent(new Event(Event.EVENT_MAINMENU));
+ return true;
+ }
+ return false;
+ case KeyEvent.KEYCODE_CAMERA:
+ case KeyEvent.KEYCODE_SEARCH:
+ _do_right_click = (kevent.getAction() == KeyEvent.ACTION_DOWN);
+ return true;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT: {
+ // HTC Hero doesn't seem to generate
+ // MotionEvent.ACTION_DOWN events on trackball press :(
+ // We'll have to just fake one here.
+ // Some other handsets lack a trackball, so the DPAD is
+ // the only way of moving the cursor.
+ int motion_action;
+ // FIXME: this logic is a mess.
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ switch (kevent.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ motion_action = MotionEvent.ACTION_DOWN;
+ break;
+ case KeyEvent.ACTION_UP:
+ motion_action = MotionEvent.ACTION_UP;
+ break;
+ default: // ACTION_MULTIPLE
+ return false;
+ }
+ } else
+ motion_action = MotionEvent.ACTION_MOVE;
+
+ Event e = new Event(getEventType(motion_action));
+ e.mouse_x = 0;
+ e.mouse_y = 0;
+ e.mouse_relative = true;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ e.mouse_y = -TRACKBALL_SCALE;
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ e.mouse_y = TRACKBALL_SCALE;
+ break;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ e.mouse_x = -TRACKBALL_SCALE;
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ e.mouse_x = TRACKBALL_SCALE;
+ break;
+ }
+ scummvm.pushEvent(e);
+ return true;
+ }
+ case KeyEvent.KEYCODE_BACK:
+ // skip isSystem() check and fall through to main code
+ break;
+ default:
+ if (kevent.isSystem())
+ return false;
+ }
+
+ // FIXME: what do I need to do for composed characters?
+
+ Event e = new Event();
+
+ switch (kevent.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ e.type = Event.EVENT_KEYDOWN;
+ e.synthetic = false;
+ break;
+ case KeyEvent.ACTION_UP:
+ e.type = Event.EVENT_KEYUP;
+ e.synthetic = false;
+ break;
+ case KeyEvent.ACTION_MULTIPLE:
+ // e.type is handled below
+ e.synthetic = true;
+ break;
+ default:
+ return false;
+ }
+
+ e.kbd_keycode = Event.androidKeyMap.containsKey(keyCode) ?
+ Event.androidKeyMap.get(keyCode) : Event.KEYCODE_INVALID;
+ e.kbd_ascii = kevent.getUnicodeChar();
+ if (e.kbd_ascii == 0)
+ e.kbd_ascii = e.kbd_keycode; // scummvm keycodes are mostly ascii
+
+
+ e.kbd_flags = 0;
+ if (kevent.isAltPressed())
+ e.kbd_flags |= Event.KBD_ALT;
+ if (kevent.isSymPressed()) // no ctrl key in android, so use sym (?)
+ e.kbd_flags |= Event.KBD_CTRL;
+ if (kevent.isShiftPressed()) {
+ if (keyCode >= KeyEvent.KEYCODE_0 &&
+ keyCode <= KeyEvent.KEYCODE_9) {
+ // Shift+number -> convert to F* key
+ int offset = keyCode == KeyEvent.KEYCODE_0 ?
+ 10 : keyCode - KeyEvent.KEYCODE_1; // turn 0 into 10
+ e.kbd_keycode = Event.KEYCODE_F1 + offset;
+ e.kbd_ascii = Event.ASCII_F1 + offset;
+ } else
+ e.kbd_flags |= Event.KBD_SHIFT;
+ }
+
+ if (kevent.getAction() == KeyEvent.ACTION_MULTIPLE) {
+ for (int i = 0; i <= kevent.getRepeatCount(); i++) {
+ e.type = Event.EVENT_KEYDOWN;
+ scummvm.pushEvent(e);
+ e.type = Event.EVENT_KEYUP;
+ scummvm.pushEvent(e);
+ }
+ } else
+ scummvm.pushEvent(e);
+
+ return true;
+ }
+
+ private int getEventType(int action) {
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ _last_click_was_right = _do_right_click;
+ return _last_click_was_right ?
+ Event.EVENT_RBUTTONDOWN : Event.EVENT_LBUTTONDOWN;
+ case MotionEvent.ACTION_UP:
+ return _last_click_was_right ?
+ Event.EVENT_RBUTTONUP : Event.EVENT_LBUTTONUP;
+ case MotionEvent.ACTION_MOVE:
+ return Event.EVENT_MOUSEMOVE;
+ default:
+ return Event.EVENT_INVALID;
+ }
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ int type = getEventType(event.getAction());
+ if (type == Event.EVENT_INVALID)
+ return false;
+
+ Event e = new Event(type);
+ e.mouse_x =
+ (int)(event.getX() * event.getXPrecision()) * TRACKBALL_SCALE;
+ e.mouse_y =
+ (int)(event.getY() * event.getYPrecision()) * TRACKBALL_SCALE;
+ e.mouse_relative = true;
+ scummvm.pushEvent(e);
+
+ return true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int type = getEventType(event.getAction());
+ if (type == Event.EVENT_INVALID)
+ return false;
+
+ Event e = new Event(type);
+ e.mouse_x = (int)event.getX();
+ e.mouse_y = (int)event.getY();
+ e.mouse_relative = false;
+ scummvm.pushEvent(e);
+
+ return true;
+ }
+
+ private void showKeyboard(boolean show) {
+ SurfaceView main_surface = (SurfaceView)findViewById(R.id.main_surface);
+ InputMethodManager imm = (InputMethodManager)
+ getSystemService(INPUT_METHOD_SERVICE);
+ if (show)
+ imm.showSoftInput(main_surface, InputMethodManager.SHOW_IMPLICIT);
+ else
+ imm.hideSoftInputFromWindow(main_surface.getWindowToken(),
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ }
+}
diff --git a/backends/platform/android/org/inodes/gus/scummvm/ScummVMApplication.java b/backends/platform/android/org/inodes/gus/scummvm/ScummVMApplication.java
new file mode 100644
index 0000000000..37a9d09e1a
--- /dev/null
+++ b/backends/platform/android/org/inodes/gus/scummvm/ScummVMApplication.java
@@ -0,0 +1,29 @@
+package org.inodes.gus.scummvm;
+
+import android.app.Application;
+
+import java.io.File;
+
+public class ScummVMApplication extends Application {
+ public final static String ACTION_PLUGIN_QUERY = "org.inodes.gus.scummvm.action.PLUGIN_QUERY";
+ public final static String EXTRA_UNPACK_LIBS = "org.inodes.gus.scummvm.extra.UNPACK_LIBS";
+
+ private static File cache_dir;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ // This is still on /data :(
+ cache_dir = getCacheDir();
+ // This is mounted noexec :(
+ //cache_dir = new File(Environment.getExternalStorageDirectory(),
+ // "/.ScummVM.tmp");
+ // This is owned by download manager and requires special
+ // permissions to access :(
+ //cache_dir = Environment.getDownloadCacheDirectory();
+ }
+
+ public static File getLastCacheDir() {
+ return cache_dir;
+ }
+}
diff --git a/backends/platform/android/org/inodes/gus/scummvm/Unpacker.java b/backends/platform/android/org/inodes/gus/scummvm/Unpacker.java
new file mode 100644
index 0000000000..efa3e1d2ef
--- /dev/null
+++ b/backends/platform/android/org/inodes/gus/scummvm/Unpacker.java
@@ -0,0 +1,370 @@
+package org.inodes.gus.scummvm;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.ProgressBar;
+
+import java.io.IOException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipEntry;
+
+public class Unpacker extends Activity {
+ private final static String META_NEXT_ACTIVITY =
+ "org.inodes.gus.unpacker.nextActivity";
+ private ProgressBar mProgress;
+ private File mUnpackDest; // location to unpack into
+ private AsyncTask<String, Integer, Void> mUnpacker;
+ private final static int REQUEST_MARKET = 1;
+
+ private static class UnpackJob {
+ public ZipFile zipfile;
+ public Set<String> paths;
+
+ public UnpackJob(ZipFile zipfile, Set<String> paths) {
+ this.zipfile = zipfile;
+ this.paths = paths;
+ }
+
+ public long UnpackSize() {
+ long size = 0;
+ for (String path: paths) {
+ ZipEntry entry = zipfile.getEntry(path);
+ if (entry != null) size += entry.getSize();
+ }
+ return size;
+ }
+ }
+
+ private class UnpackTask extends AsyncTask<String, Integer, Void> {
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ mProgress.setIndeterminate(false);
+ mProgress.setMax(progress[1]);
+ mProgress.setProgress(progress[0]);
+ mProgress.postInvalidate();
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ Bundle md = getMetaData();
+ String nextActivity = md.getString(META_NEXT_ACTIVITY);
+ if (nextActivity != null) {
+ final ComponentName cn =
+ ComponentName.unflattenFromString(nextActivity);
+ if (cn != null) {
+ final Intent origIntent = getIntent();
+ Intent intent = new Intent();
+ intent.setPackage(origIntent.getPackage());
+ intent.setComponent(cn);
+ if (origIntent.getExtras() != null)
+ intent.putExtras(origIntent.getExtras());
+ intent.putExtra(Intent.EXTRA_INTENT, origIntent);
+ intent.setDataAndType(origIntent.getData(),
+ origIntent.getType());
+ //intent.fillIn(getIntent(), 0);
+ intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ Log.i(this.toString(),
+ "Starting next activity with intent " + intent);
+ startActivity(intent);
+ } else {
+ Log.w(this.toString(),
+ "Unable to extract a component name from " + nextActivity);
+ }
+ }
+
+ finish();
+ }
+
+ @Override
+ protected Void doInBackground(String... all_libs) {
+ // This will contain all unpack jobs
+ Map<String, UnpackJob> unpack_jobs =
+ new HashMap<String, UnpackJob>(all_libs.length);
+
+ // This will contain all unpack filenames (so we can
+ // detect stale files in the unpack directory)
+ Set<String> all_files = new HashSet<String>(all_libs.length);
+
+ for (String lib: all_libs) {
+ final Uri uri = Uri.parse(lib);
+ final String pkg = uri.getAuthority();
+ final String path = uri.getPath().substring(1); // skip first /
+
+ all_files.add(new File(path).getName());
+
+ UnpackJob job = unpack_jobs.get(pkg);
+ if (job == null) {
+ try {
+ // getPackageResourcePath is hidden in Context,
+ // but exposed in ContextWrapper...
+ ContextWrapper context =
+ new ContextWrapper(createPackageContext(pkg, 0));
+ ZipFile zipfile =
+ new ZipFile(context.getPackageResourcePath());
+ job = new UnpackJob(zipfile, new HashSet<String>(1));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(this.toString(), "Package " + pkg +
+ " not found", e);
+ continue;
+ } catch (IOException e) {
+ // FIXME: show some sort of GUI error dialog
+ Log.e(this.toString(),
+ "Error opening ZIP for package " + pkg, e);
+ continue;
+ }
+ unpack_jobs.put(pkg, job);
+ }
+ job.paths.add(path);
+ }
+
+ // Delete stale filenames from mUnpackDest
+ for (File file: mUnpackDest.listFiles()) {
+ if (!all_files.contains(file.getName())) {
+ Log.i(this.toString(),
+ "Deleting stale cached file " + file);
+ file.delete();
+ }
+ }
+
+ int total_size = 0;
+ for (UnpackJob job: unpack_jobs.values())
+ total_size += job.UnpackSize();
+
+ publishProgress(0, total_size);
+
+ mUnpackDest.mkdirs();
+
+ int progress = 0;
+
+ for (UnpackJob job: unpack_jobs.values()) {
+ try {
+ ZipFile zipfile = job.zipfile;
+ for (String path: job.paths) {
+ ZipEntry zipentry = zipfile.getEntry(path);
+ if (zipentry == null)
+ throw new FileNotFoundException(
+ "Couldn't find " + path + " in zip");
+ File dest = new File(mUnpackDest, new File(path).getName());
+ if (dest.exists() &&
+ dest.lastModified() == zipentry.getTime() &&
+ dest.length() == zipentry.getSize()) {
+ // Already unpacked
+ progress += zipentry.getSize();
+ } else {
+ if (dest.exists())
+ Log.d(this.toString(),
+ "Replacing " + dest.getPath() +
+ " old.mtime=" + dest.lastModified() +
+ " new.mtime=" + zipentry.getTime() +
+ " old.size=" + dest.length() +
+ " new.size=" + zipentry.getSize());
+ else
+ Log.i(this.toString(),
+ "Extracting " + zipentry.getName() +
+ " from " + zipfile.getName() +
+ " to " + dest.getPath());
+
+ long next_update = progress;
+
+ InputStream in = zipfile.getInputStream(zipentry);
+ OutputStream out = new FileOutputStream(dest);
+ int len;
+ byte[] buffer = new byte[4096];
+ while ((len = in.read(buffer)) != -1) {
+ out.write(buffer, 0, len);
+ progress += len;
+ if (progress >= next_update) {
+ publishProgress(progress, total_size);
+ // Arbitrary limit of 2% update steps
+ next_update += total_size / 50;
+ }
+ }
+
+ in.close();
+ out.close();
+ dest.setLastModified(zipentry.getTime());
+ }
+ publishProgress(progress, total_size);
+ }
+
+ zipfile.close();
+ } catch (IOException e) {
+ // FIXME: show some sort of GUI error dialog
+ Log.e(this.toString(), "Error unpacking plugin", e);
+ }
+ }
+
+ if (progress != total_size)
+ Log.d(this.toString(), "Ended with progress " + progress +
+ " != total size " + total_size);
+
+ setResult(RESULT_OK);
+
+ return null;
+ }
+ }
+
+ private class PluginBroadcastReciever extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!intent.getAction()
+ .equals(ScummVMApplication.ACTION_PLUGIN_QUERY)) {
+ Log.e(this.toString(),
+ "Received unexpected action " + intent.getAction());
+ return;
+ }
+
+ Bundle extras = getResultExtras(false);
+ if (extras == null) {
+ // Nothing for us to do.
+ Unpacker.this.setResult(RESULT_OK);
+ finish();
+ }
+
+ ArrayList<String> unpack_libs =
+ extras.getStringArrayList(ScummVMApplication.EXTRA_UNPACK_LIBS);
+
+ if (unpack_libs != null && !unpack_libs.isEmpty()) {
+ final String[] libs =
+ unpack_libs.toArray(new String[unpack_libs.size()]);
+ mUnpacker = new UnpackTask().execute(libs);
+ }
+ }
+ }
+
+ private void initPlugins() {
+ Bundle extras = new Bundle(1);
+
+ ArrayList<String> unpack_libs = new ArrayList<String>(1);
+ // This is the common ScummVM code (not really a "plugin" as such)
+ unpack_libs.add(new Uri.Builder()
+ .scheme("plugin")
+ .authority(getPackageName())
+ .path("mylib/armeabi/libscummvm.so")
+ .toString());
+ extras.putStringArrayList(ScummVMApplication.EXTRA_UNPACK_LIBS,
+ unpack_libs);
+
+ Intent intent = new Intent(ScummVMApplication.ACTION_PLUGIN_QUERY);
+ sendOrderedBroadcast(intent, Manifest.permission.SCUMMVM_PLUGIN,
+ new PluginBroadcastReciever(),
+ null, RESULT_OK, null, extras);
+ }
+
+ @Override
+ public void onCreate(Bundle b) {
+ super.onCreate(b);
+
+ mUnpackDest = ScummVMApplication.getLastCacheDir();
+
+ setContentView(R.layout.splash);
+ mProgress = (ProgressBar)findViewById(R.id.progress);
+
+ setResult(RESULT_CANCELED);
+
+ tryUnpack();
+ }
+
+ private void tryUnpack() {
+ Intent intent = new Intent(ScummVMApplication.ACTION_PLUGIN_QUERY);
+ List<ResolveInfo> plugins = getPackageManager()
+ .queryBroadcastReceivers(intent, 0);
+ if (plugins.isEmpty()) {
+ // No plugins installed
+ AlertDialog.Builder alert = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_plugins_title)
+ .setMessage(R.string.no_plugins_found)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ })
+ .setNegativeButton(R.string.quit,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+
+ final Uri uri = Uri.parse("market://search?q=ScummVM plugin");
+ final Intent market_intent = new Intent(Intent.ACTION_VIEW, uri);
+ if (getPackageManager().resolveActivity(market_intent, 0) != null) {
+ alert.setPositiveButton(R.string.to_market,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ try {
+ startActivityForResult(market_intent,
+ REQUEST_MARKET);
+ } catch (ActivityNotFoundException e) {
+ Log.e(this.toString(),
+ "Error starting market", e);
+ }
+ }
+ });
+ }
+
+ alert.show();
+
+ } else {
+ // Already have at least one plugin installed
+ initPlugins();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mUnpacker != null)
+ mUnpacker.cancel(true);
+ super.onStop();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent data) {
+ switch (requestCode) {
+ case REQUEST_MARKET:
+ if (resultCode != RESULT_OK)
+ Log.w(this.toString(), "Market returned " + resultCode);
+ tryUnpack();
+ break;
+ }
+ }
+
+ private Bundle getMetaData() {
+ try {
+ ActivityInfo ai = getPackageManager()
+ .getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
+ return ai.metaData;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(this.toString(), "Unable to find my own meta-data", e);
+ return new Bundle();
+ }
+ }
+}