diff options
author | Max Horn | 2010-06-06 09:34:36 +0000 |
---|---|---|
committer | Max Horn | 2010-06-06 09:34:36 +0000 |
commit | 46155b2c3678784f6333eed1d65a35eefdcb2001 (patch) | |
tree | 1f570683935a5bede0e2475493a4f48b1548d320 /backends/platform/android/org | |
parent | 3efec5720de2c46355c323763dee96b719ed5aa1 (diff) | |
download | scummvm-rg350-46155b2c3678784f6333eed1d65a35eefdcb2001.tar.gz scummvm-rg350-46155b2c3678784f6333eed1d65a35eefdcb2001.tar.bz2 scummvm-rg350-46155b2c3678784f6333eed1d65a35eefdcb2001.zip |
Add Android backend from patch #2603856
svn-id: r49449
Diffstat (limited to 'backends/platform/android/org')
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(); + } + } +} |