aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backends/platform/android/README.build84
-rw-r--r--backends/platform/android/android.cpp1413
-rw-r--r--backends/platform/android/android.mk52
-rw-r--r--backends/platform/android/asset-archive.cpp414
-rw-r--r--backends/platform/android/asset-archive.h53
-rw-r--r--backends/platform/android/module.mk85
-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
-rw-r--r--backends/platform/android/scummvm-android-themeengine.patch135
-rw-r--r--base/commandLine.cpp4
-rw-r--r--common/textconsole.cpp12
-rwxr-xr-xconfigure43
-rw-r--r--dists/android/mkmanifest.pl169
-rw-r--r--dists/android/res/drawable/gradient.xml7
-rw-r--r--dists/android/res/layout/main.xml10
-rw-r--r--dists/android/res/layout/splash.xml19
-rw-r--r--dists/android/res/values/strings.xml22
-rw-r--r--sound/decoders/vorbis.cpp2
-rwxr-xr-xtools/update-version.pl1
24 files changed, 4121 insertions, 7 deletions
diff --git a/backends/platform/android/README.build b/backends/platform/android/README.build
new file mode 100644
index 0000000000..fa56bfc180
--- /dev/null
+++ b/backends/platform/android/README.build
@@ -0,0 +1,84 @@
+Building the ScummVM Android port
+=================================
+
+You will need these things to build:
+1. Android EGL headers and library
+2. Android SDK
+3. An arm-android-eabi GCC toolchain
+
+In the example commands, we are going to build against the Android 1.5
+native ABI (but using the Android 1.6 SDK tools). Other version
+combinations might/should be possible with a bit of tweaking.
+
+In detail:
+
+1. Android EGL headers and library
+
+You can build these from the full Android source, but it is far easier
+to just download the 3 Android EGL headers from here:
+ http://android.git.kernel.org/?p=platform/frameworks/base.git;a=tree;f=opengl/include/EGL;hb=HEAD
+ (copy them to a directory called "EGL" somewhere)
+
+... and grab libEGL.so off an existing phone/emulator:
+ adb pull /system/lib/libEGL.so /tmp
+
+2. Android SDK
+
+Download and install somewhere.
+
+3. arm-android-eabi GCC toolchain
+
+You have several choices for toolchains:
+
+- Use Google arm-eabi prebuilt toolchain.
+
+This is shipped with both the Android source release and Android NDK.
+The problem is that "arm-eabi-gcc" can't actually link anything
+successfully without extra command line flags. To use this with the
+ScummVM configure/build environment you will need to create a family
+of shell wrapper scripts that convert "arm-android-eabi-foo" to
+"arm-eabi-foo -mandroid".
+
+For example, I use this script:
+ #!/bin/sh
+ exec arm-eabi-${0##*-} -mandroid -DANDROID "$@"
+
+... and create a family of symlinks/hardlinks pointing to it called
+arm-android-eabi-gcc, arm-android-eabi-g++, etc. For tools that don't
+take a "-mandroid" argument - like arm-eabi-strip - I bypass the shell
+wrapper and just create an arm-android-eabi-strip symlink to the tool
+directly.
+
+- Build your own arm-android-eabi toolchain from GCC source.
+
+This is lots of fun. I suggest my Android openembedded patches, see:
+ http://wiki.github.com/anguslees/openembedded-android/
+(You just need to have lots of disk space and type a few commands)
+If you get stuck, ask
+
+Alternatively, do a websearch - there are several other cross-compile
+toolchains around.
+
+
+Building ScummVM
+================
+
+ export ANDROID_SDK=<root of Android SDK>
+
+ PATH=$ANDROID_SDK/platforms/android-1.6/tools:$ANDROID_SDK/tools:$PATH
+ # You also want to ensure your arm-android-eabi toolchain is in your $PATH
+
+ export ANDROID_TOP=<root of built Android source>
+
+ EGL_INC="-I<location of EGL/ header directory>"
+ EGL_LIBS="-L<location of libEGL.so>"
+
+ CPPFLAGS="$EGL_INC" \
+ LDFLAGS="-g $EGL_LIBS" \
+ ./configure --backend=android --host=android --enable-zlib #and any other flags
+ make scummvm.apk
+
+This will build a "monolithic" ScummVM package, with the engines
+statically linked in. If you want to build separate engine packages,
+like on the market, add "--enable-plugins --default-dynamic" to
+configure and also make scummvm-engine-scumm.apk, etc.
diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp
new file mode 100644
index 0000000000..76590ec823
--- /dev/null
+++ b/backends/platform/android/android.cpp
@@ -0,0 +1,1413 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *
+ */
+
+#include "backends/base-backend.h"
+#include "base/main.h"
+#include "graphics/surface.h"
+
+#include "backends/platform/android/video.h"
+
+#if defined(ANDROID_BACKEND)
+
+#define ANDROID_VERSION_GE(major,minor) \
+ (ANDROID_MAJOR_VERSION > (major) || \
+ (ANDROID_MAJOR_VERSION == (major) && ANDROID_MINOR_VERSION >= (minor)))
+
+#include <jni.h>
+
+#include <string.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <sys/time.h>
+#include <time.h>
+
+#include <GLES/gl.h>
+#include <GLES/glext.h>
+#include <EGL/egl.h>
+#include <android/log.h>
+
+#include "common/archive.h"
+#include "common/util.h"
+#include "common/rect.h"
+#include "common/queue.h"
+#include "common/mutex.h"
+#include "common/events.h"
+#include "common/config-manager.h"
+
+#include "backends/fs/posix/posix-fs-factory.h"
+#include "backends/keymapper/keymapper.h"
+#include "backends/saves/default/default-saves.h"
+#include "backends/timer/default/default-timer.h"
+#include "backends/plugins/posix/posix-provider.h"
+#include "sound/mixer_intern.h"
+
+#include "backends/platform/android/asset-archive.h"
+
+#undef LOG_TAG
+#define LOG_TAG "ScummVM"
+
+#if 0
+#define ENTER(args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, args)
+#else
+#define ENTER(args...) /**/
+#endif
+
+// Fix JNIEXPORT declaration to actually do something useful
+#undef JNIEXPORT
+#define JNIEXPORT __attribute__ ((visibility("default")))
+
+static JavaVM *cached_jvm;
+static jfieldID FID_Event_type;
+static jfieldID FID_Event_synthetic;
+static jfieldID FID_Event_kbd_keycode;
+static jfieldID FID_Event_kbd_ascii;
+static jfieldID FID_Event_kbd_flags;
+static jfieldID FID_Event_mouse_x;
+static jfieldID FID_Event_mouse_y;
+static jfieldID FID_Event_mouse_relative;
+static jfieldID FID_ScummVM_nativeScummVM;
+static jmethodID MID_Object_wait;
+
+JNIEnv* JNU_GetEnv() {
+ JNIEnv* env;
+ bool version_unsupported =
+ cached_jvm->GetEnv((void**)&env, JNI_VERSION_1_2);
+ assert(! version_unsupported);
+ return env;
+}
+
+static void JNU_ThrowByName(JNIEnv* env, const char* name, const char* msg) {
+ jclass cls = env->FindClass(name);
+ // if cls is NULL, an exception has already been thrown
+ if (cls != NULL)
+ env->ThrowNew(cls, msg);
+ env->DeleteLocalRef(cls);
+}
+
+// floating point. use sparingly.
+template <class T>
+static inline T scalef(T in, float numerator, float denominator) {
+ return static_cast<float>(in) * numerator / denominator;
+}
+
+static inline GLfixed xdiv(int numerator, int denominator) {
+ assert(numerator < (1<<16));
+ return (numerator << 16) / denominator;
+}
+
+#ifdef DYNAMIC_MODULES
+class AndroidPluginProvider : public POSIXPluginProvider {
+protected:
+ virtual void addCustomDirectories(Common::FSList &dirs) const;
+};
+#endif
+
+
+#if 0
+#define CHECK_GL_ERROR() checkGlError(__FILE__, __LINE__)
+static const char* getGlErrStr(GLenum error) {
+ switch (error) {
+ case GL_NO_ERROR: return "GL_NO_ERROR";
+ case GL_INVALID_ENUM: return "GL_INVALID_ENUM";
+ case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
+ case GL_STACK_OVERFLOW: return "GL_STACK_OVERFLOW";
+ case GL_STACK_UNDERFLOW: return "GL_STACK_UNDERFLOW";
+ case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
+ }
+
+ static char buf[40];
+ snprintf(buf, sizeof(buf), "(Unknown GL error code 0x%x)", error);
+ return buf;
+}
+static void checkGlError(const char* file, int line) {
+ GLenum error = glGetError();
+ if (error != GL_NO_ERROR)
+ warning("%s:%d: GL error: %s", file, line, getGlErrStr(error));
+}
+#else
+#define CHECK_GL_ERROR() do {} while (false)
+#endif
+
+class OSystem_Android : public BaseBackend {
+private:
+ jobject _back_ptr; // back pointer to (java) peer instance
+ jmethodID MID_displayMessageOnOSD;
+ jmethodID MID_setWindowCaption;
+ jmethodID MID_initBackend;
+ jmethodID MID_audioSampleRate;
+ jmethodID MID_showVirtualKeyboard;
+ jmethodID MID_getSysArchives;
+ jmethodID MID_getPluginDirectories;
+ jmethodID MID_setupScummVMSurface;
+ jmethodID MID_destroyScummVMSurface;
+
+ int _screen_changeid;
+ EGLDisplay _egl_display;
+ EGLSurface _egl_surface;
+ EGLint _egl_surface_width;
+ EGLint _egl_surface_height;
+
+ bool _force_redraw;
+
+ // Game layer
+ GLESPaletteTexture* _game_texture;
+ int _shake_offset;
+ bool _full_screen_dirty;
+ Common::Array<Common::Rect> _dirty_rects;
+
+ // Overlay layer
+ GLES4444Texture* _overlay_texture;
+ bool _show_overlay;
+
+ // Mouse layer
+ GLESPaletteATexture* _mouse_texture;
+ Common::Point _mouse_hotspot;
+ int _mouse_targetscale;
+ bool _show_mouse;
+ bool _use_mouse_palette;
+
+ Common::Queue<Common::Event> _event_queue;
+ MutexRef _event_queue_lock;
+
+ bool _timer_thread_exit;
+ pthread_t _timer_thread;
+ static void* timerThreadFunc(void* arg);
+
+ bool _virtkeybd_on;
+
+ Common::SaveFileManager *_savefile;
+ Audio::MixerImpl *_mixer;
+ Common::TimerManager *_timer;
+ FilesystemFactory *_fsFactory;
+ Common::Archive *_asset_archive;
+ timeval _startTime;
+
+ void setupScummVMSurface();
+ void destroyScummVMSurface();
+ void setupKeymapper();
+ void _setCursorPalette(const byte *colors, uint start, uint num);
+
+public:
+ OSystem_Android(jobject am);
+ virtual ~OSystem_Android();
+ bool initJavaHooks(JNIEnv* env, jobject self);
+
+ static OSystem_Android* fromJavaObject(JNIEnv* env, jobject obj);
+ virtual void initBackend();
+ void addPluginDirectories(Common::FSList &dirs) const;
+
+ virtual bool hasFeature(Feature f);
+ virtual void setFeatureState(Feature f, bool enable);
+ virtual bool getFeatureState(Feature f);
+ virtual const GraphicsMode *getSupportedGraphicsModes() const;
+ virtual int getDefaultGraphicsMode() const;
+ bool setGraphicsMode(const char *name);
+ virtual bool setGraphicsMode(int mode);
+ virtual int getGraphicsMode() const;
+ virtual void initSize(uint width, uint height,
+ const Graphics::PixelFormat *format);
+ virtual int getScreenChangeID() const { return _screen_changeid; }
+ virtual int16 getHeight();
+ virtual int16 getWidth();
+ virtual void setPalette(const byte *colors, uint start, uint num);
+ virtual void grabPalette(byte *colors, uint start, uint num);
+ virtual void copyRectToScreen(const byte *buf, int pitch, int x, int y, int w, int h);
+ virtual void updateScreen();
+ virtual Graphics::Surface *lockScreen();
+ virtual void unlockScreen();
+ virtual void setShakePos(int shakeOffset);
+ virtual void fillScreen(uint32 col);
+ virtual void setFocusRectangle(const Common::Rect& rect);
+ virtual void clearFocusRectangle();
+
+ virtual void showOverlay();
+ virtual void hideOverlay();
+ virtual void clearOverlay();
+ virtual void grabOverlay(OverlayColor *buf, int pitch);
+ virtual void copyRectToOverlay(const OverlayColor *buf, int pitch, int x, int y, int w, int h);
+ virtual int16 getOverlayHeight();
+ virtual int16 getOverlayWidth();
+ virtual Graphics::PixelFormat getOverlayFormat() const {
+ // RGBA 4444
+ Graphics::PixelFormat format;
+ format.bytesPerPixel = 2;
+ format.rLoss = 8 - 4;
+ format.gLoss = 8 - 4;
+ format.bLoss = 8 - 4;
+ format.aLoss = 8 - 4;
+ format.rShift = 3*4;
+ format.gShift = 2*4;
+ format.bShift = 1*4;
+ format.aShift = 0*4;
+ return format;
+ }
+
+ virtual bool showMouse(bool visible);
+
+ virtual void warpMouse(int x, int y);
+ virtual void setMouseCursor(const byte *buf, uint w, uint h, int hotspotX, int hotspotY, uint32 keycolor, int cursorTargetScale, const Graphics::PixelFormat *format);
+ virtual void setCursorPalette(const byte *colors, uint start, uint num);
+ virtual void disableCursorPalette(bool disable);
+
+ virtual bool pollEvent(Common::Event &event);
+ void pushEvent(const Common::Event& event);
+ virtual uint32 getMillis();
+ virtual void delayMillis(uint msecs);
+
+ virtual MutexRef createMutex(void);
+ virtual void lockMutex(MutexRef mutex);
+ virtual void unlockMutex(MutexRef mutex);
+ virtual void deleteMutex(MutexRef mutex);
+
+ virtual void quit();
+
+ virtual void setWindowCaption(const char *caption);
+ virtual void displayMessageOnOSD(const char *msg);
+ virtual void showVirtualKeyboard(bool enable);
+
+ virtual Common::SaveFileManager *getSavefileManager();
+ virtual Audio::Mixer *getMixer();
+ virtual void getTimeAndDate(TimeDate &t) const;
+ virtual Common::TimerManager *getTimerManager();
+ virtual FilesystemFactory *getFilesystemFactory();
+ virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0);
+};
+
+OSystem_Android::OSystem_Android(jobject am)
+ : _back_ptr(0),
+ _egl_display(EGL_NO_DISPLAY),
+ _egl_surface(EGL_NO_SURFACE),
+ _screen_changeid(0),
+ _force_redraw(false),
+ _game_texture(NULL),
+ _overlay_texture(NULL),
+ _mouse_texture(NULL),
+ _use_mouse_palette(false),
+ _show_mouse(false),
+ _show_overlay(false),
+ _savefile(0),
+ _mixer(0),
+ _timer(0),
+ _fsFactory(new POSIXFilesystemFactory()),
+ _asset_archive(new AndroidAssetArchive(am)),
+ _shake_offset(0),
+ _full_screen_dirty(false),
+ _event_queue_lock(createMutex()) {
+}
+
+OSystem_Android::~OSystem_Android() {
+ ENTER("~OSystem_Android()");
+ delete _game_texture;
+ delete _overlay_texture;
+ delete _mouse_texture;
+ destroyScummVMSurface();
+ JNIEnv* env = JNU_GetEnv();
+ //env->DeleteWeakGlobalRef(_back_ptr);
+ env->DeleteGlobalRef(_back_ptr);
+ delete _savefile;
+ delete _mixer;
+ delete _timer;
+ delete _fsFactory;
+ delete _asset_archive;
+ deleteMutex(_event_queue_lock);
+}
+
+OSystem_Android* OSystem_Android::fromJavaObject(JNIEnv* env, jobject obj) {
+ jlong peer = env->GetLongField(obj, FID_ScummVM_nativeScummVM);
+ return (OSystem_Android*)peer;
+}
+
+bool OSystem_Android::initJavaHooks(JNIEnv* env, jobject self) {
+ // weak global ref to allow class to be unloaded
+ // ... except dalvik doesn't implement NewWeakGlobalRef (yet)
+ //_back_ptr = env->NewWeakGlobalRef(self);
+ _back_ptr = env->NewGlobalRef(self);
+
+ jclass cls = env->GetObjectClass(_back_ptr);
+
+#define FIND_METHOD(name, signature) do { \
+ MID_ ## name = env->GetMethodID(cls, #name, signature); \
+ if (MID_ ## name == NULL) \
+ return false; \
+ } while (0)
+
+ FIND_METHOD(setWindowCaption, "(Ljava/lang/String;)V");
+ FIND_METHOD(displayMessageOnOSD, "(Ljava/lang/String;)V");
+ FIND_METHOD(initBackend, "()V");
+ FIND_METHOD(audioSampleRate, "()I");
+ FIND_METHOD(showVirtualKeyboard, "(Z)V");
+ FIND_METHOD(getSysArchives, "()[Ljava/lang/String;");
+ FIND_METHOD(getPluginDirectories, "()[Ljava/lang/String;");
+ FIND_METHOD(setupScummVMSurface, "()V");
+ FIND_METHOD(destroyScummVMSurface, "()V");
+
+#undef FIND_METHOD
+
+ return true;
+}
+
+static void ScummVM_create(JNIEnv* env, jobject self, jobject am) {
+ OSystem_Android* cpp_obj = new OSystem_Android(am);
+ if (!cpp_obj->initJavaHooks(env, self))
+ // Exception already thrown by initJavaHooks
+ return;
+
+ env->SetLongField(self, FID_ScummVM_nativeScummVM, (jlong)cpp_obj);
+
+#ifdef DYNAMIC_MODULES
+ PluginManager::instance().addPluginProvider(new AndroidPluginProvider());
+#endif
+}
+
+static void ScummVM_nativeDestroy(JNIEnv* env, jobject self) {
+ OSystem_Android* cpp_obj = OSystem_Android::fromJavaObject(env, self);
+ delete cpp_obj;
+}
+
+static void ScummVM_audioMixCallback(JNIEnv* env, jobject self,
+ jbyteArray jbuf) {
+ OSystem_Android* cpp_obj = OSystem_Android::fromJavaObject(env, self);
+ jsize len = env->GetArrayLength(jbuf);
+ jbyte* buf = env->GetByteArrayElements(jbuf, NULL);
+ if (buf == NULL) {
+ warning("Unable to get Java audio byte array. Skipping.");
+ return;
+ }
+ Audio::MixerImpl* mixer =
+ static_cast<Audio::MixerImpl*>(cpp_obj->getMixer());
+ assert(mixer);
+ mixer->mixCallback(reinterpret_cast<byte*>(buf), len);
+ env->ReleaseByteArrayElements(jbuf, buf, 0);
+}
+
+static void ScummVM_setConfManInt(JNIEnv* env, jclass cls,
+ jstring key_obj, jint value) {
+ ENTER("setConfManInt(%p, %d)", key_obj, (int)value);
+ const char* key = env->GetStringUTFChars(key_obj, NULL);
+ if (key == NULL)
+ return;
+ ConfMan.setInt(key, value);
+ env->ReleaseStringUTFChars(key_obj, key);
+}
+
+static void ScummVM_setConfManString(JNIEnv* env, jclass cls, jstring key_obj,
+ jstring value_obj) {
+ ENTER("setConfManStr(%p, %p)", key_obj, value_obj);
+ const char* key = env->GetStringUTFChars(key_obj, NULL);
+ if (key == NULL)
+ return;
+ const char* value = env->GetStringUTFChars(value_obj, NULL);
+ if (value == NULL) {
+ env->ReleaseStringUTFChars(key_obj, key);
+ return;
+ }
+ ConfMan.set(key, value);
+ env->ReleaseStringUTFChars(value_obj, value);
+ env->ReleaseStringUTFChars(key_obj, key);
+}
+
+void* OSystem_Android::timerThreadFunc(void* arg) {
+ OSystem_Android* system = (OSystem_Android*)arg;
+ DefaultTimerManager* timer = (DefaultTimerManager*)(system->_timer);
+
+ struct timespec tv;
+ tv.tv_sec = 0;
+ tv.tv_nsec = 100 * 1000 * 1000; // 100ms
+
+ while (!system->_timer_thread_exit) {
+ timer->handler();
+ nanosleep(&tv, NULL);
+ }
+
+ return NULL;
+}
+
+void OSystem_Android::initBackend() {
+ ENTER("initBackend()");
+ JNIEnv* env = JNU_GetEnv();
+
+ ConfMan.setInt("autosave_period", 0);
+ ConfMan.setInt("FM_medium_quality", true);
+
+ // must happen before creating TimerManager to avoid race in
+ // creating EventManager
+ setupKeymapper();
+
+ // BUG: "transient" ConfMan settings get nuked by the options
+ // screen. Passing the savepath in this way makes it stick
+ // (via ConfMan.registerDefault)
+ _savefile = new DefaultSaveFileManager(ConfMan.get("savepath"));
+ _timer = new DefaultTimerManager();
+
+ gettimeofday(&_startTime, NULL);
+
+ jint sample_rate = env->CallIntMethod(_back_ptr, MID_audioSampleRate);
+ if (env->ExceptionCheck()) {
+ warning("Error finding audio sample rate - assuming 11025HZ");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ sample_rate = 11025;
+ }
+ _mixer = new Audio::MixerImpl(this, sample_rate);
+ _mixer->setReady(true);
+
+ env->CallVoidMethod(_back_ptr, MID_initBackend);
+ if (env->ExceptionCheck()) {
+ error("Error in Java initBackend");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+
+ _timer_thread_exit = false;
+ pthread_create(&_timer_thread, NULL, timerThreadFunc, this);
+
+ OSystem::initBackend();
+
+ setupScummVMSurface();
+}
+
+void OSystem_Android::addPluginDirectories(Common::FSList &dirs) const {
+ ENTER("OSystem_Android::addPluginDirectories()");
+ JNIEnv* env = JNU_GetEnv();
+
+ jobjectArray array =
+ (jobjectArray)env->CallObjectMethod(_back_ptr, MID_getPluginDirectories);
+ if (env->ExceptionCheck()) {
+ warning("Error finding plugin directories");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return;
+ }
+
+ jsize size = env->GetArrayLength(array);
+ for (jsize i = 0; i < size; ++i) {
+ jstring path_obj = (jstring)env->GetObjectArrayElement(array, i);
+ if (path_obj == NULL)
+ continue;
+ const char* path = env->GetStringUTFChars(path_obj, NULL);
+ if (path == NULL) {
+ warning("Error getting string characters from plugin directory");
+ env->ExceptionClear();
+ env->DeleteLocalRef(path_obj);
+ continue;
+ }
+ dirs.push_back(Common::FSNode(path));
+ env->ReleaseStringUTFChars(path_obj, path);
+ env->DeleteLocalRef(path_obj);
+ }
+}
+
+bool OSystem_Android::hasFeature(Feature f) {
+ return (f == kFeatureCursorHasPalette ||
+ f == kFeatureVirtualKeyboard ||
+ f == kFeatureOverlaySupportsAlpha);
+}
+
+void OSystem_Android::setFeatureState(Feature f, bool enable) {
+ ENTER("setFeatureState(%d, %d)", f, enable);
+ switch (f) {
+ case kFeatureVirtualKeyboard:
+ _virtkeybd_on = enable;
+ showVirtualKeyboard(enable);
+ break;
+ default:
+ break;
+ }
+}
+
+bool OSystem_Android::getFeatureState(Feature f) {
+ switch (f) {
+ case kFeatureVirtualKeyboard:
+ return _virtkeybd_on;
+ default:
+ return false;
+ }
+}
+
+const OSystem::GraphicsMode* OSystem_Android::getSupportedGraphicsModes() const {
+ static const OSystem::GraphicsMode s_supportedGraphicsModes[] = {
+ {"default", "Default", 1},
+ {0, 0, 0},
+ };
+ return s_supportedGraphicsModes;
+}
+
+
+int OSystem_Android::getDefaultGraphicsMode() const {
+ return 1;
+}
+
+bool OSystem_Android::setGraphicsMode(const char *mode) {
+ ENTER("setGraphicsMode(%s)", mode);
+ return true;
+}
+
+bool OSystem_Android::setGraphicsMode(int mode) {
+ ENTER("setGraphicsMode(%d)", mode);
+ return true;
+}
+
+int OSystem_Android::getGraphicsMode() const {
+ return 1;
+}
+
+void OSystem_Android::setupScummVMSurface() {
+ JNIEnv* env = JNU_GetEnv();
+ env->CallVoidMethod(_back_ptr, MID_setupScummVMSurface);
+ if (env->ExceptionCheck())
+ return;
+
+ // EGL set up with a new surface. Initialise OpenGLES context.
+
+ _egl_display = eglGetCurrentDisplay();
+ _egl_surface = eglGetCurrentSurface(EGL_DRAW);
+
+ static bool log_version = true;
+ if (log_version) {
+ __android_log_print(ANDROID_LOG_INFO, LOG_TAG,
+ "Using EGL %s (%s); GL %s/%s (%s)",
+ eglQueryString(_egl_display, EGL_VERSION),
+ eglQueryString(_egl_display, EGL_VENDOR),
+ glGetString(GL_VERSION),
+ glGetString(GL_RENDERER),
+ glGetString(GL_VENDOR));
+ log_version = false; // only log this once
+ }
+
+ GLESTexture::initGLExtensions();
+
+ if (!eglQuerySurface(_egl_display, _egl_surface,
+ EGL_WIDTH, &_egl_surface_width) ||
+ !eglQuerySurface(_egl_display, _egl_surface,
+ EGL_HEIGHT, &_egl_surface_height)) {
+ JNU_ThrowByName(env, "java/lang/RuntimeException",
+ "Error fetching EGL surface width/height");
+ return;
+ }
+ __android_log_print(ANDROID_LOG_INFO, LOG_TAG,
+ "New surface is %dx%d",
+ _egl_surface_width, _egl_surface_height);
+
+ CHECK_GL_ERROR();
+
+ // Turn off anything that looks like 3D ;)
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_LIGHTING);
+ glDisable(GL_FOG);
+ glDisable(GL_DITHER);
+ glShadeModel(GL_FLAT);
+ glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ glEnableClientState(GL_VERTEX_ARRAY);
+ glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+
+ glEnable(GL_TEXTURE_2D);
+
+ if (!_game_texture)
+ _game_texture = new GLESPaletteTexture();
+ else
+ _game_texture->reinitGL();
+
+ if (!_overlay_texture)
+ _overlay_texture = new GLES4444Texture();
+ else
+ _overlay_texture->reinitGL();
+
+ if (!_mouse_texture)
+ _mouse_texture = new GLESPaletteATexture();
+ else
+ _mouse_texture->reinitGL();
+
+ glViewport(0, 0, _egl_surface_width, _egl_surface_height);
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glOrthof(0, _egl_surface_width, _egl_surface_height, 0, -1, 1);
+
+ glMatrixMode(GL_MODELVIEW);
+ glLoadIdentity();
+
+ CHECK_GL_ERROR();
+
+ _force_redraw = true;
+}
+
+void OSystem_Android::destroyScummVMSurface() {
+ _egl_surface = EGL_NO_SURFACE;
+ JNIEnv* env = JNU_GetEnv();
+ env->CallVoidMethod(_back_ptr, MID_destroyScummVMSurface);
+ // Can't use OpenGLES functions after this
+}
+
+void OSystem_Android::initSize(uint width, uint height,
+ const Graphics::PixelFormat *format) {
+ ENTER("initSize(%d,%d,%p)", width, height, format);
+
+ _game_texture->allocBuffer(width, height);
+
+ // Cap at 320x200 or the ScummVM themes abort :/
+ GLuint overlay_width = MIN(_egl_surface_width, 320);
+ GLuint overlay_height = MIN(_egl_surface_height, 200);
+ _overlay_texture->allocBuffer(overlay_width, overlay_height);
+
+ // Don't know mouse size yet - it gets reallocated in
+ // setMouseCursor. We need the palette allocated before
+ // setMouseCursor however, so just take a guess at the desired
+ // size (it's small).
+ _mouse_texture->allocBuffer(20, 20);
+}
+
+int16 OSystem_Android::getHeight() {
+ return _game_texture->height();
+}
+
+int16 OSystem_Android::getWidth() {
+ return _game_texture->width();
+}
+
+void OSystem_Android::setPalette(const byte* colors, uint start, uint num) {
+ ENTER("setPalette(%p, %u, %u)", colors, start, num);
+
+ if (!_use_mouse_palette)
+ _setCursorPalette(colors, start, num);
+
+ byte* palette = _game_texture->palette() + start*3;
+ do {
+ for (int i = 0; i < 3; ++i)
+ palette[i] = colors[i];
+ palette += 3;
+ colors += 4;
+ } while (--num);
+}
+
+void OSystem_Android::grabPalette(byte *colors, uint start, uint num) {
+ ENTER("grabPalette(%p, %u, %u)", colors, start, num);
+ const byte* palette = _game_texture->palette_const() + start*3;
+ do {
+ for (int i = 0; i < 3; ++i)
+ colors[i] = palette[i];
+ colors[3] = 0xff; // alpha
+
+ palette += 3;
+ colors += 4;
+ } while (--num);
+}
+
+void OSystem_Android::copyRectToScreen(const byte *buf, int pitch,
+ int x, int y, int w, int h) {
+ ENTER("copyRectToScreen(%p, %d, %d, %d, %d, %d)",
+ buf, pitch, x, y, w, h);
+
+ _game_texture->updateBuffer(x, y, w, h, buf, pitch);
+}
+
+void OSystem_Android::updateScreen() {
+ //ENTER("updateScreen()");
+
+ if (!_force_redraw &&
+ !_game_texture->dirty() &&
+ !_overlay_texture->dirty() &&
+ !_mouse_texture->dirty())
+ return;
+
+ _force_redraw = false;
+
+ glPushMatrix();
+
+ if (_shake_offset != 0) {
+ // This is the only case where _game_texture doesn't
+ // cover the entire screen.
+ glClearColorx(0, 0, 0, 1 << 16);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Move everything up by _shake_offset (game) pixels
+ glTranslatex(0, -_shake_offset << 16, 0);
+ }
+
+ _game_texture->drawTexture(0, 0,
+ _egl_surface_width, _egl_surface_height);
+
+ CHECK_GL_ERROR();
+
+ if (_show_overlay) {
+ _overlay_texture->drawTexture(0, 0,
+ _egl_surface_width,
+ _egl_surface_height);
+ CHECK_GL_ERROR();
+ }
+
+ if (_show_mouse) {
+ glPushMatrix();
+
+ glTranslatex(-_mouse_hotspot.x << 16,
+ -_mouse_hotspot.y << 16,
+ 0);
+
+ // Scale up ScummVM -> OpenGL (pixel) coordinates
+ int texwidth, texheight;
+ if (_show_overlay) {
+ texwidth = getOverlayWidth();
+ texheight = getOverlayHeight();
+ } else {
+ texwidth = getWidth();
+ texheight = getHeight();
+ }
+ glScalex(xdiv(_egl_surface_width, texwidth),
+ xdiv(_egl_surface_height, texheight),
+ 1 << 16);
+
+ // Note the extra half texel to position the mouse in
+ // the middle of the x,y square:
+ const Common::Point& mouse = getEventManager()->getMousePos();
+ glTranslatex((mouse.x << 16) | 1 << 15,
+ (mouse.y << 16) | 1 << 15, 0);
+
+ // Mouse targetscale just seems to make the cursor way
+ // too big :/
+ //glScalex(_mouse_targetscale << 16, _mouse_targetscale << 16,
+ // 1 << 16);
+
+ _mouse_texture->drawTexture();
+
+ glPopMatrix();
+ }
+
+ glPopMatrix();
+
+ CHECK_GL_ERROR();
+
+ if (!eglSwapBuffers(_egl_display, _egl_surface)) {
+ EGLint error = eglGetError();
+ warning("eglSwapBuffers exited with error 0x%x", error);
+ // Some errors mean we need to reinit GL
+ if (error == EGL_CONTEXT_LOST) {
+ destroyScummVMSurface();
+ setupScummVMSurface();
+ }
+ }
+}
+
+Graphics::Surface *OSystem_Android::lockScreen() {
+ ENTER("lockScreen()");
+ Graphics::Surface* surface = _game_texture->surface();
+ assert(surface->pixels);
+ return surface;
+}
+
+void OSystem_Android::unlockScreen() {
+ ENTER("unlockScreen()");
+ assert(_game_texture->dirty());
+}
+
+void OSystem_Android::setShakePos(int shake_offset) {
+ ENTER("setShakePos(%d)", shake_offset);
+ if (_shake_offset != shake_offset) {
+ _shake_offset = shake_offset;
+ _force_redraw = true;
+ }
+}
+
+void OSystem_Android::fillScreen(uint32 col) {
+ ENTER("fillScreen(%u)", col);
+ assert(col < 256);
+ _game_texture->fillBuffer(col);
+}
+
+void OSystem_Android::setFocusRectangle(const Common::Rect& rect) {
+ ENTER("setFocusRectangle(%d,%d,%d,%d)",
+ rect.left, rect.top, rect.right, rect.bottom);
+#if 0
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glOrthof(rect.left, rect.right, rect.top, rect.bottom, 0, 1);
+ glMatrixMode(GL_MODELVIEW);
+
+ _force_redraw = true;
+#endif
+}
+
+void OSystem_Android::clearFocusRectangle() {
+ ENTER("clearFocusRectangle()");
+#if 0
+ glMatrixMode(GL_PROJECTION);
+ glLoadIdentity();
+ glOrthof(0, _egl_surface_width, _egl_surface_height, 0, -1, 1);
+ glMatrixMode(GL_MODELVIEW);
+
+ _force_redraw = true;
+#endif
+}
+
+void OSystem_Android::showOverlay() {
+ ENTER("showOverlay()");
+ _show_overlay = true;
+ _force_redraw = true;
+}
+
+void OSystem_Android::hideOverlay() {
+ ENTER("hideOverlay()");
+ _show_overlay = false;
+ _force_redraw = true;
+}
+
+void OSystem_Android::clearOverlay() {
+ ENTER("clearOverlay()");
+ _overlay_texture->fillBuffer(0);
+}
+
+void OSystem_Android::grabOverlay(OverlayColor *buf, int pitch) {
+ ENTER("grabOverlay(%p, %d)", buf, pitch);
+ // We support overlay alpha blending, so the pixel data here
+ // shouldn't actually be used. Let's fill it with zeros, I'm sure
+ // it will be fine...
+ const Graphics::Surface* surface = _overlay_texture->surface_const();
+ assert(surface->bytesPerPixel == sizeof(buf[0]));
+ int h = surface->h;
+ do {
+ memset(buf, 0, surface->w * sizeof(buf[0]));
+ buf += pitch; // This 'pitch' is pixels not bytes
+ } while (--h);
+}
+
+void OSystem_Android::copyRectToOverlay(const OverlayColor *buf, int pitch,
+ int x, int y, int w, int h) {
+ ENTER("copyRectToOverlay(%p, %d, %d, %d, %d, %d)",
+ buf, pitch, x, y, w, h);
+ const Graphics::Surface* surface = _overlay_texture->surface_const();
+ assert(surface->bytesPerPixel == sizeof(buf[0]));
+
+ // This 'pitch' is pixels not bytes
+ _overlay_texture->updateBuffer(x, y, w, h, buf, pitch * sizeof(buf[0]));
+}
+
+int16 OSystem_Android::getOverlayHeight() {
+ return _overlay_texture->height();
+}
+
+int16 OSystem_Android::getOverlayWidth() {
+ return _overlay_texture->width();
+}
+
+bool OSystem_Android::showMouse(bool visible) {
+ ENTER("showMouse(%d)", visible);
+ _show_mouse = visible;
+ return true;
+}
+
+void OSystem_Android::warpMouse(int x, int y) {
+ ENTER("warpMouse(%d, %d)", x, y);
+ // We use only the eventmanager's idea of the current mouse
+ // position, so there is nothing extra to do here.
+}
+
+void OSystem_Android::setMouseCursor(const byte *buf, uint w, uint h,
+ int hotspotX, int hotspotY,
+ uint32 keycolor, int cursorTargetScale,
+ const Graphics::PixelFormat *format) {
+ ENTER("setMouseCursor(%p, %u, %u, %d, %d, %d, %d, %p)",
+ buf, w, h, hotspotX, hotspotY, (int)keycolor, cursorTargetScale,
+ format);
+
+ assert(keycolor < 256);
+
+ _mouse_texture->allocBuffer(w, h);
+
+ // Update palette alpha based on keycolor
+ byte* palette = _mouse_texture->palette();
+ int i = 256;
+ do {
+ palette[3] = 0xff;
+ palette += 4;
+ } while (--i);
+ palette = _mouse_texture->palette();
+ palette[keycolor*4 + 3] = 0x00;
+ _mouse_texture->updateBuffer(0, 0, w, h, buf, w);
+
+ _mouse_hotspot = Common::Point(hotspotX, hotspotY);
+ _mouse_targetscale = cursorTargetScale;
+}
+
+void OSystem_Android::_setCursorPalette(const byte *colors,
+ uint start, uint num) {
+ byte* palette = _mouse_texture->palette() + start*4;
+ do {
+ for (int i = 0; i < 3; ++i)
+ palette[i] = colors[i];
+ // Leave alpha untouched to preserve keycolor
+
+ palette += 4;
+ colors += 4;
+ } while (--num);
+}
+
+void OSystem_Android::setCursorPalette(const byte *colors,
+ uint start, uint num) {
+ ENTER("setCursorPalette(%p, %u, %u)", colors, start, num);
+ _setCursorPalette(colors, start, num);
+ _use_mouse_palette = true;
+}
+
+void OSystem_Android::disableCursorPalette(bool disable) {
+ ENTER("disableCursorPalette(%d)", disable);
+ _use_mouse_palette = !disable;
+}
+
+void OSystem_Android::setupKeymapper() {
+#ifdef ENABLE_KEYMAPPER
+ using namespace Common;
+
+ Keymapper *mapper = getEventManager()->getKeymapper();
+
+ HardwareKeySet *keySet = new HardwareKeySet();
+ keySet->addHardwareKey(
+ new HardwareKey("n", KeyState(KEYCODE_n), "n (vk)",
+ kTriggerLeftKeyType,
+ kVirtualKeyboardActionType));
+ mapper->registerHardwareKeySet(keySet);
+
+ Keymap *globalMap = new Keymap("global");
+ Action *act;
+
+ act = new Action(globalMap, "VIRT", "Display keyboard",
+ kVirtualKeyboardActionType);
+ act->addKeyEvent(KeyState(KEYCODE_F7, ASCII_F7, 0));
+
+ mapper->addGlobalKeymap(globalMap);
+
+ mapper->pushKeymap("global");
+#endif
+}
+
+bool OSystem_Android::pollEvent(Common::Event &event) {
+ //ENTER("pollEvent()");
+ lockMutex(_event_queue_lock);
+ if (_event_queue.empty()) {
+ unlockMutex(_event_queue_lock);
+ return false;
+ }
+ event = _event_queue.pop();
+ unlockMutex(_event_queue_lock);
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ // TODO: only dirty/redraw move bounds
+ _force_redraw = true;
+ // fallthrough
+ case Common::EVENT_LBUTTONDOWN:
+ case Common::EVENT_LBUTTONUP:
+ case Common::EVENT_RBUTTONDOWN:
+ case Common::EVENT_RBUTTONUP:
+ case Common::EVENT_WHEELUP:
+ case Common::EVENT_WHEELDOWN:
+ case Common::EVENT_MBUTTONDOWN:
+ case Common::EVENT_MBUTTONUP: {
+ if (event.kbd.flags == 1) { // relative mouse hack
+ // Relative (trackball) mouse hack.
+ const Common::Point& mouse_pos =
+ getEventManager()->getMousePos();
+ event.mouse.x += mouse_pos.x;
+ event.mouse.y += mouse_pos.y;
+ event.mouse.x = CLIP(event.mouse.x, (int16)0, _show_overlay ?
+ getOverlayWidth() : getWidth());
+ event.mouse.y = CLIP(event.mouse.y, (int16)0, _show_overlay ?
+ getOverlayHeight() : getHeight());
+ } else {
+ // Touchscreen events need to be converted
+ // from device to game coords first.
+ const GLESTexture* tex = _show_overlay
+ ? static_cast<GLESTexture*>(_overlay_texture)
+ : static_cast<GLESTexture*>(_game_texture);
+ event.mouse.x = scalef(event.mouse.x, tex->width(),
+ _egl_surface_width);
+ event.mouse.y = scalef(event.mouse.y, tex->height(),
+ _egl_surface_height);
+ event.mouse.x -= _shake_offset;
+ }
+ break;
+ }
+ case Common::EVENT_SCREEN_CHANGED:
+ debug("EVENT_SCREEN_CHANGED");
+ _screen_changeid++;
+ destroyScummVMSurface();
+ setupScummVMSurface();
+ break;
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void OSystem_Android::pushEvent(const Common::Event& event) {
+ lockMutex(_event_queue_lock);
+
+ // Try to combine multiple queued mouse move events
+ if (event.type == Common::EVENT_MOUSEMOVE &&
+ !_event_queue.empty() &&
+ _event_queue.back().type == Common::EVENT_MOUSEMOVE) {
+ Common::Event tail = _event_queue.back();
+ if (event.kbd.flags) {
+ // relative movement hack
+ tail.mouse.x += event.mouse.x;
+ tail.mouse.y += event.mouse.y;
+ } else {
+ // absolute position
+ tail.kbd.flags = 0; // clear relative flag
+ tail.mouse.x = event.mouse.x;
+ tail.mouse.y = event.mouse.y;
+ }
+ }
+ else
+ _event_queue.push(event);
+
+ unlockMutex(_event_queue_lock);
+}
+
+static void ScummVM_pushEvent(JNIEnv* env, jobject self, jobject java_event) {
+ OSystem_Android* cpp_obj = OSystem_Android::fromJavaObject(env, self);
+
+ Common::Event event;
+ event.type = (Common::EventType)env->GetIntField(java_event,
+ FID_Event_type);
+ event.synthetic =
+ env->GetBooleanField(java_event, FID_Event_synthetic);
+
+ switch (event.type) {
+ case Common::EVENT_KEYDOWN:
+ case Common::EVENT_KEYUP:
+ event.kbd.keycode = (Common::KeyCode)env->GetIntField(
+ java_event, FID_Event_kbd_keycode);
+ event.kbd.ascii = static_cast<int>(env->GetIntField(
+ java_event, FID_Event_kbd_ascii));
+ event.kbd.flags = static_cast<int>(env->GetIntField(
+ java_event, FID_Event_kbd_flags));
+ break;
+ case Common::EVENT_MOUSEMOVE:
+ case Common::EVENT_LBUTTONDOWN:
+ case Common::EVENT_LBUTTONUP:
+ case Common::EVENT_RBUTTONDOWN:
+ case Common::EVENT_RBUTTONUP:
+ case Common::EVENT_WHEELUP:
+ case Common::EVENT_WHEELDOWN:
+ case Common::EVENT_MBUTTONDOWN:
+ case Common::EVENT_MBUTTONUP:
+ event.mouse.x =
+ env->GetIntField(java_event, FID_Event_mouse_x);
+ event.mouse.y =
+ env->GetIntField(java_event, FID_Event_mouse_y);
+ // This is a terrible hack. We stash "relativeness"
+ // in the kbd.flags field until pollEvent() can work
+ // it out.
+ event.kbd.flags = env->GetBooleanField(
+ java_event, FID_Event_mouse_relative) ? 1 : 0;
+ break;
+ default:
+ break;
+ }
+
+ cpp_obj->pushEvent(event);
+}
+
+uint32 OSystem_Android::getMillis() {
+ timeval curTime;
+ gettimeofday(&curTime, NULL);
+ return (uint32)(((curTime.tv_sec - _startTime.tv_sec) * 1000) + \
+ ((curTime.tv_usec - _startTime.tv_usec) / 1000));
+}
+
+void OSystem_Android::delayMillis(uint msecs) {
+ usleep(msecs * 1000);
+}
+
+OSystem::MutexRef OSystem_Android::createMutex() {
+ pthread_mutexattr_t attr;
+ pthread_mutexattr_init(&attr);
+ pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
+
+ pthread_mutex_t *mutex = new pthread_mutex_t;
+ if (pthread_mutex_init(mutex, &attr) != 0) {
+ warning("pthread_mutex_init() failed!");
+ delete mutex;
+ return NULL;
+ }
+ return (MutexRef)mutex;
+}
+
+void OSystem_Android::lockMutex(MutexRef mutex) {
+ if (pthread_mutex_lock((pthread_mutex_t*)mutex) != 0)
+ warning("pthread_mutex_lock() failed!");
+}
+
+void OSystem_Android::unlockMutex(MutexRef mutex) {
+ if (pthread_mutex_unlock((pthread_mutex_t*)mutex) != 0)
+ warning("pthread_mutex_unlock() failed!");
+}
+
+void OSystem_Android::deleteMutex(MutexRef mutex) {
+ pthread_mutex_t* m = (pthread_mutex_t*)mutex;
+ if (pthread_mutex_destroy(m) != 0)
+ warning("pthread_mutex_destroy() failed!");
+ else
+ delete m;
+}
+
+void OSystem_Android::quit() {
+ ENTER("quit()");
+
+ _timer_thread_exit = true;
+ pthread_join(_timer_thread, NULL);
+}
+
+void OSystem_Android::setWindowCaption(const char *caption) {
+ ENTER("setWindowCaption(%s)", caption);
+ JNIEnv* env = JNU_GetEnv();
+ jstring java_caption = env->NewStringUTF(caption);
+ env->CallVoidMethod(_back_ptr, MID_setWindowCaption, java_caption);
+ if (env->ExceptionCheck()) {
+ warning("Failed to set window caption");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+ env->DeleteLocalRef(java_caption);
+}
+
+void OSystem_Android::displayMessageOnOSD(const char *msg) {
+ ENTER("displayMessageOnOSD(%s)", msg);
+ JNIEnv* env = JNU_GetEnv();
+ jstring java_msg = env->NewStringUTF(msg);
+ env->CallVoidMethod(_back_ptr, MID_displayMessageOnOSD, java_msg);
+ if (env->ExceptionCheck()) {
+ warning("Failed to display OSD message");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+ env->DeleteLocalRef(java_msg);
+}
+
+void OSystem_Android::showVirtualKeyboard(bool enable) {
+ ENTER("showVirtualKeyboard(%d)", enable);
+ JNIEnv* env = JNU_GetEnv();
+ env->CallVoidMethod(_back_ptr, MID_showVirtualKeyboard, enable);
+ if (env->ExceptionCheck()) {
+ error("Error trying to show virtual keyboard");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+}
+
+Common::SaveFileManager *OSystem_Android::getSavefileManager() {
+ assert(_savefile);
+ return _savefile;
+}
+
+Audio::Mixer *OSystem_Android::getMixer() {
+ assert(_mixer);
+ return _mixer;
+}
+
+Common::TimerManager *OSystem_Android::getTimerManager() {
+ assert(_timer);
+ return _timer;
+}
+
+void OSystem_Android::getTimeAndDate(TimeDate &td) const {
+ struct tm tm;
+ const time_t curTime = time(NULL);
+ localtime_r(&curTime, &tm);
+ td.tm_sec = tm.tm_sec;
+ td.tm_min = tm.tm_min;
+ td.tm_hour = tm.tm_hour;
+ td.tm_mday = tm.tm_mday;
+ td.tm_mon = tm.tm_mon;
+ td.tm_year = tm.tm_year;
+}
+
+FilesystemFactory *OSystem_Android::getFilesystemFactory() {
+ return _fsFactory;
+}
+
+void OSystem_Android::addSysArchivesToSearchSet(Common::SearchSet &s,
+ int priority) {
+ s.add("ASSET", _asset_archive, priority, false);
+
+ JNIEnv* env = JNU_GetEnv();
+
+ jobjectArray array =
+ (jobjectArray)env->CallObjectMethod(_back_ptr, MID_getSysArchives);
+ if (env->ExceptionCheck()) {
+ warning("Error finding system archive path");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return;
+ }
+
+ jsize size = env->GetArrayLength(array);
+ for (jsize i = 0; i < size; ++i) {
+ jstring path_obj = (jstring)env->GetObjectArrayElement(array, i);
+ const char* path = env->GetStringUTFChars(path_obj, NULL);
+ if (path != NULL) {
+ s.addDirectory(path, path, priority);
+ env->ReleaseStringUTFChars(path_obj, path);
+ }
+ env->DeleteLocalRef(path_obj);
+ }
+}
+
+
+static jint ScummVM_scummVMMain(JNIEnv* env, jobject self, jobjectArray args) {
+ OSystem_Android* cpp_obj = OSystem_Android::fromJavaObject(env, self);
+
+ const int MAX_NARGS = 32;
+ int res = -1;
+
+ int argc = env->GetArrayLength(args);
+ if (argc > MAX_NARGS) {
+ JNU_ThrowByName(env, "java/lang/IllegalArgumentException",
+ "too many arguments");
+ return 0;
+ }
+
+ char* argv[MAX_NARGS];
+ int nargs; // note use in cleanup loop below
+ for (nargs = 0; nargs < argc; ++nargs) {
+ jstring arg = (jstring)env->GetObjectArrayElement(args, nargs);
+ if (arg == NULL) {
+ argv[nargs] = NULL;
+ } else {
+ const char* cstr = env->GetStringUTFChars(arg, NULL);
+ argv[nargs] = const_cast<char*>(cstr);
+ if (cstr == NULL)
+ goto cleanup; // exception already thrown
+ }
+ env->DeleteLocalRef(arg);
+ }
+
+ g_system = cpp_obj;
+ assert(g_system);
+ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,
+ "Entering scummvm_main with %d args", argc);
+ res = scummvm_main(argc, argv);
+ __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "Exiting scummvm_main");
+ g_system->quit();
+
+cleanup:
+ nargs--;
+ for (int i = 0; i < nargs; ++i) {
+ if (argv[i] == NULL)
+ continue;
+ jstring arg = (jstring)env->GetObjectArrayElement(args, nargs);
+ if (arg == NULL)
+ // Exception already thrown
+ return res;
+ env->ReleaseStringUTFChars(arg, argv[i]);
+ env->DeleteLocalRef(arg);
+ }
+
+ return res;
+}
+
+#ifdef DYNAMIC_MODULES
+void AndroidPluginProvider::addCustomDirectories(Common::FSList &dirs) const {
+ OSystem_Android* g_system_android = (OSystem_Android*)g_system;
+ g_system_android->addPluginDirectories(dirs);
+}
+#endif
+
+const static JNINativeMethod gMethods[] = {
+ { "create", "(Landroid/content/res/AssetManager;)V",
+ (void*)ScummVM_create },
+ { "nativeDestroy", "()V", (void*)ScummVM_nativeDestroy },
+ { "scummVMMain", "([Ljava/lang/String;)I",
+ (void*)ScummVM_scummVMMain },
+ { "pushEvent", "(Lorg/inodes/gus/scummvm/Event;)V",
+ (void*)ScummVM_pushEvent },
+ { "audioMixCallback", "([B)V",
+ (void*)ScummVM_audioMixCallback },
+ { "setConfMan", "(Ljava/lang/String;I)V",
+ (void*)ScummVM_setConfManInt },
+ { "setConfMan", "(Ljava/lang/String;Ljava/lang/String;)V",
+ (void*)ScummVM_setConfManString },
+};
+
+JNIEXPORT jint JNICALL
+JNI_OnLoad(JavaVM* jvm, void* reserved) {
+ cached_jvm = jvm;
+
+ JNIEnv* env;
+ if (jvm->GetEnv((void**)&env, JNI_VERSION_1_2))
+ return JNI_ERR;
+
+ jclass cls = env->FindClass("org/inodes/gus/scummvm/ScummVM");
+ if (cls == NULL)
+ return JNI_ERR;
+ if (env->RegisterNatives(cls, gMethods, ARRAYSIZE(gMethods)) < 0)
+ return JNI_ERR;
+
+ FID_ScummVM_nativeScummVM = env->GetFieldID(cls, "nativeScummVM", "J");
+ if (FID_ScummVM_nativeScummVM == NULL)
+ return JNI_ERR;
+
+ jclass event = env->FindClass("org/inodes/gus/scummvm/Event");
+ if (event == NULL)
+ return JNI_ERR;
+ FID_Event_type = env->GetFieldID(event, "type", "I");
+ if (FID_Event_type == NULL)
+ return JNI_ERR;
+ FID_Event_synthetic = env->GetFieldID(event, "synthetic", "Z");
+ if (FID_Event_synthetic == NULL)
+ return JNI_ERR;
+ FID_Event_kbd_keycode = env->GetFieldID(event, "kbd_keycode", "I");
+ if (FID_Event_kbd_keycode == NULL)
+ return JNI_ERR;
+ FID_Event_kbd_ascii = env->GetFieldID(event, "kbd_ascii", "I");
+ if (FID_Event_kbd_ascii == NULL)
+ return JNI_ERR;
+ FID_Event_kbd_flags = env->GetFieldID(event, "kbd_flags", "I");
+ if (FID_Event_kbd_flags == NULL)
+ return JNI_ERR;
+ FID_Event_mouse_x = env->GetFieldID(event, "mouse_x", "I");
+ if (FID_Event_mouse_x == NULL)
+ return JNI_ERR;
+ FID_Event_mouse_y = env->GetFieldID(event, "mouse_y", "I");
+ if (FID_Event_mouse_y == NULL)
+ return JNI_ERR;
+ FID_Event_mouse_relative = env->GetFieldID(event, "mouse_relative", "Z");
+ if (FID_Event_mouse_relative == NULL)
+ return JNI_ERR;
+
+ cls = env->FindClass("java/lang/Object");
+ if (cls == NULL)
+ return JNI_ERR;
+ MID_Object_wait = env->GetMethodID(cls, "wait", "()V");
+ if (MID_Object_wait == NULL)
+ return JNI_ERR;
+
+ return JNI_VERSION_1_2;
+}
+
+#endif
diff --git a/backends/platform/android/android.mk b/backends/platform/android/android.mk
new file mode 100644
index 0000000000..0bc8fa265e
--- /dev/null
+++ b/backends/platform/android/android.mk
@@ -0,0 +1,52 @@
+# Android specific build targets
+
+AAPT = aapt
+DX = dx
+APKBUILDER = apkbuilder
+ADB = adb -e
+ANDROID_JAR = $(ANDROID_SDK)/platforms/android-1.6/android.jar
+JAVAC ?= javac
+JAVACFLAGS = -source 1.5 -target 1.5
+
+# FIXME: find/mark plugin entry points and add all this back again:
+#LDFLAGS += -Wl,--gc-sections
+#CXXFLAGS += -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden
+
+scummvm.apk: build.tmp/libscummvm.so resources.ap_ classes.dex
+ # Package installer won't delete old libscummvm.so on upgrade so
+ # replace it with a zero size file
+ $(INSTALL) -d build.stage/common/lib/armeabi
+ touch build.stage/common/lib/armeabi/libscummvm.so
+ # We now handle the library unpacking ourselves from mylib/
+ $(INSTALL) -d build.stage/common/mylib/armeabi
+ $(INSTALL) -c -m 644 build.tmp/libscummvm.so build.stage/common/mylib/armeabi/
+ $(STRIP) build.stage/common/mylib/armeabi/libscummvm.so
+ # "-nf lib/armeabi/libscummvm.so" builds bogus paths?
+ $(APKBUILDER) $@ -z resources.ap_ -f classes.dex -rf build.stage/common || { $(RM) $@; exit 1; }
+
+scummvm-engine-%.apk: plugins/lib%.so build.tmp/%/resources.ap_ build.tmp/plugins/classes.dex
+ $(INSTALL) -d build.stage/$*/apk/mylib/armeabi/
+ $(INSTALL) -c -m 644 plugins/lib$*.so build.stage/$*/apk/mylib/armeabi/
+ $(STRIP) build.stage/$*/apk/mylib/armeabi/lib$*.so
+ $(APKBUILDER) $@ -z build.tmp/$*/resources.ap_ -f build.tmp/plugins/classes.dex -rf build.stage/$*/apk || { $(RM) $@; exit 1; }
+
+release/%.apk: %.apk
+ @$(MKDIR) -p $(@D)
+ @$(RM) $@
+ $(CP) $< $@.tmp
+ # remove debugging signature
+ zip -d $@.tmp META-INF/\*
+ jarsigner $(JARSIGNER_FLAGS) $@.tmp release
+ zipalign 4 $@.tmp $@
+ $(RM) $@.tmp
+
+androidrelease: release/scummvm.apk $(patsubst plugins/lib%.so,release/scummvm-engine-%.apk,$(PLUGINS))
+
+androidtest: scummvm.apk scummvm-engine-scumm.apk scummvm-engine-kyra.apk
+ @set -e; for apk in $^; do \
+ echo $(ADB) install -r $$apk; \
+ $(ADB) install -r $$apk; \
+ done
+ $(ADB) shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n org.inodes.gus.scummvm/.Unpacker
+
+.PHONY: androidrelease androidtest
diff --git a/backends/platform/android/asset-archive.cpp b/backends/platform/android/asset-archive.cpp
new file mode 100644
index 0000000000..20c6a653c0
--- /dev/null
+++ b/backends/platform/android/asset-archive.cpp
@@ -0,0 +1,414 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *
+ */
+
+#if defined(ANDROID)
+
+#include <jni.h>
+
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "common/str.h"
+#include "common/stream.h"
+#include "common/util.h"
+#include "common/archive.h"
+#include "common/debug.h"
+
+#include "backends/platform/android/asset-archive.h"
+
+extern JNIEnv* JNU_GetEnv();
+
+// Must match android.content.res.AssetManager.ACCESS_*
+const jint ACCESS_UNKNOWN = 0;
+const jint ACCESS_RANDOM = 1;
+
+// This might be useful to someone else. Assumes markSupported() == true.
+class JavaInputStream : public Common::SeekableReadStream {
+public:
+ JavaInputStream(JNIEnv* env, jobject is);
+ virtual ~JavaInputStream();
+ virtual bool eos() const { return _eos; }
+ virtual bool err() const { return _err; }
+ virtual void clearErr() { _eos = _err = false; }
+ virtual uint32 read(void *dataPtr, uint32 dataSize);
+ virtual int32 pos() const { return _pos; }
+ virtual int32 size() const { return _len; }
+ virtual bool seek(int32 offset, int whence = SEEK_SET);
+private:
+ void close(JNIEnv* env);
+ jmethodID MID_mark;
+ jmethodID MID_available;
+ jmethodID MID_close;
+ jmethodID MID_read;
+ jmethodID MID_reset;
+ jmethodID MID_skip;
+ jobject _input_stream;
+ jsize _buflen;
+ jbyteArray _buf;
+ uint32 _pos;
+ jint _len;
+ bool _eos;
+ bool _err;
+};
+
+JavaInputStream::JavaInputStream(JNIEnv* env, jobject is) :
+ _eos(false), _err(false), _pos(0)
+{
+ _input_stream = env->NewGlobalRef(is);
+ _buflen = 8192;
+ _buf = static_cast<jbyteArray>(env->NewGlobalRef(env->NewByteArray(_buflen)));
+
+ jclass cls = env->GetObjectClass(_input_stream);
+ MID_mark = env->GetMethodID(cls, "mark", "(I)V");
+ assert(MID_mark);
+ MID_available = env->GetMethodID(cls, "available", "()I");
+ assert(MID_mark);
+ MID_close = env->GetMethodID(cls, "close", "()V");
+ assert(MID_close);
+ MID_read = env->GetMethodID(cls, "read", "([BII)I");
+ assert(MID_read);
+ MID_reset = env->GetMethodID(cls, "reset", "()V");
+ assert(MID_reset);
+ MID_skip = env->GetMethodID(cls, "skip", "(J)J");
+ assert(MID_skip);
+
+ // Mark start of stream, so we can reset back to it.
+ // readlimit is set to something bigger than anything we might
+ // want to seek within.
+ env->CallVoidMethod(_input_stream, MID_mark, 10*1024*1024);
+ _len = env->CallIntMethod(_input_stream, MID_available);
+}
+
+JavaInputStream::~JavaInputStream() {
+ JNIEnv* env = JNU_GetEnv();
+ close(env);
+ env->DeleteGlobalRef(_buf);
+ env->DeleteGlobalRef(_input_stream);
+}
+
+void JavaInputStream::close(JNIEnv* env) {
+ env->CallVoidMethod(_input_stream, MID_close);
+ if (env->ExceptionCheck())
+ env->ExceptionClear();
+}
+
+uint32 JavaInputStream::read(void *dataPtr, uint32 dataSize) {
+ JNIEnv* env = JNU_GetEnv();
+
+ if (_buflen < dataSize) {
+ _buflen = dataSize;
+ env->DeleteGlobalRef(_buf);
+ _buf = static_cast<jbyteArray>(env->NewGlobalRef(env->NewByteArray(_buflen)));
+ }
+
+ jint ret = env->CallIntMethod(_input_stream, MID_read, _buf, 0, dataSize);
+ if (env->ExceptionCheck()) {
+ warning("Exception during JavaInputStream::read(%p, %d)",
+ dataPtr, dataSize);
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ _err = true;
+ ret = -1;
+ } else if (ret == -1) {
+ _eos = true;
+ ret = 0;
+ } else {
+ env->GetByteArrayRegion(_buf, 0, ret, static_cast<jbyte*>(dataPtr));
+ _pos += ret;
+ }
+ return ret;
+}
+
+bool JavaInputStream::seek(int32 offset, int whence) {
+ JNIEnv* env = JNU_GetEnv();
+ uint32 newpos;
+ switch (whence) {
+ case SEEK_SET:
+ newpos = offset;
+ break;
+ case SEEK_CUR:
+ newpos = _pos + offset;
+ break;
+ case SEEK_END:
+ newpos = _len + offset;
+ break;
+ default:
+ debug("Unknown 'whence' arg %d", whence);
+ return false;
+ }
+
+ jlong skip_bytes;
+ if (newpos > _pos) {
+ skip_bytes = newpos - _pos;
+ } else {
+ // Can't skip backwards, so jump back to start and skip from there.
+ env->CallVoidMethod(_input_stream, MID_reset);
+ if (env->ExceptionCheck()) {
+ warning("Failed to rewind to start of asset stream");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return false;
+ }
+ _pos = 0;
+ skip_bytes = newpos;
+ }
+
+ while (skip_bytes > 0) {
+ jlong ret = env->CallLongMethod(_input_stream, MID_skip, skip_bytes);
+ if (env->ExceptionCheck()) {
+ warning("Failed to skip %ld bytes into asset stream",
+ static_cast<long>(skip_bytes));
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return false;
+ } else if (ret == 0) {
+ warning("InputStream->skip(%ld) didn't skip any bytes. Aborting seek.",
+ static_cast<long>(skip_bytes));
+ return false; // No point looping forever...
+ }
+ _pos += ret;
+ skip_bytes -= ret;
+ }
+ _eos = false;
+ return true;
+}
+
+
+// Must match android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH
+const jlong UNKNOWN_LENGTH = -1;
+
+// Reading directly from a fd is so much more efficient, that it is
+// worth optimising for.
+class AssetFdReadStream : public Common::SeekableReadStream {
+public:
+ AssetFdReadStream(JNIEnv* env, jobject assetfd);
+ virtual ~AssetFdReadStream();
+ virtual bool eos() const { return _eos; }
+ virtual bool err() const { return _err; }
+ virtual void clearErr() { _eos = _err = false; }
+ virtual uint32 read(void *dataPtr, uint32 dataSize);
+ virtual int32 pos() const { return _pos; }
+ virtual int32 size() const { return _declared_len; }
+ virtual bool seek(int32 offset, int whence = SEEK_SET);
+private:
+ void close(JNIEnv* env);
+ int _fd;
+ jmethodID MID_close;
+ jobject _assetfd;
+ jlong _start_off;
+ jlong _declared_len;
+ uint32 _pos;
+ bool _eos;
+ bool _err;
+};
+
+AssetFdReadStream::AssetFdReadStream(JNIEnv* env, jobject assetfd) :
+ _eos(false), _err(false), _pos(0)
+{
+ _assetfd = env->NewGlobalRef(assetfd);
+
+ jclass cls = env->GetObjectClass(_assetfd);
+ MID_close = env->GetMethodID(cls, "close", "()V");
+ assert(MID_close);
+
+ jmethodID MID_getStartOffset =
+ env->GetMethodID(cls, "getStartOffset", "()J");
+ assert(MID_getStartOffset);
+ _start_off = env->CallLongMethod(_assetfd, MID_getStartOffset);
+
+ jmethodID MID_getDeclaredLength =
+ env->GetMethodID(cls, "getDeclaredLength", "()J");
+ assert(MID_getDeclaredLength);
+ _declared_len = env->CallLongMethod(_assetfd, MID_getDeclaredLength);
+
+ jmethodID MID_getFileDescriptor =
+ env->GetMethodID(cls, "getFileDescriptor", "()Ljava/io/FileDescriptor;");
+ assert(MID_getFileDescriptor);
+ jobject javafd = env->CallObjectMethod(_assetfd, MID_getFileDescriptor);
+ assert(javafd);
+ jclass fd_cls = env->GetObjectClass(javafd);
+ jfieldID FID_descriptor = env->GetFieldID(fd_cls, "descriptor", "I");
+ assert(FID_descriptor);
+ _fd = env->GetIntField(javafd, FID_descriptor);
+}
+
+AssetFdReadStream::~AssetFdReadStream() {
+ JNIEnv* env = JNU_GetEnv();
+ env->CallVoidMethod(_assetfd, MID_close);
+ if (env->ExceptionCheck())
+ env->ExceptionClear();
+ env->DeleteGlobalRef(_assetfd);
+}
+
+uint32 AssetFdReadStream::read(void *dataPtr, uint32 dataSize) {
+ if (_declared_len != UNKNOWN_LENGTH) {
+ jlong cap = _declared_len - _pos;
+ if (dataSize > cap)
+ dataSize = cap;
+ }
+ int ret = ::read(_fd, dataPtr, dataSize);
+ if (ret == 0)
+ _eos = true;
+ else if (ret == -1)
+ _err = true;
+ else
+ _pos += ret;
+ return ret;
+}
+
+bool AssetFdReadStream::seek(int32 offset, int whence) {
+ if (whence == SEEK_SET) {
+ if (_declared_len != UNKNOWN_LENGTH && offset > _declared_len)
+ offset = _declared_len;
+ offset += _start_off;
+ } else if (whence == SEEK_END && _declared_len != UNKNOWN_LENGTH) {
+ whence = SEEK_SET;
+ offset = _start_off + _declared_len + offset;
+ }
+ int ret = lseek(_fd, offset, whence);
+ if (ret == -1)
+ return false;
+ _pos = ret - _start_off;
+ _eos = false;
+ return true;
+}
+
+AndroidAssetArchive::AndroidAssetArchive(jobject am) {
+ JNIEnv* env = JNU_GetEnv();
+ _am = env->NewGlobalRef(am);
+
+ jclass cls = env->GetObjectClass(_am);
+ MID_open = env->GetMethodID(cls, "open",
+ "(Ljava/lang/String;I)Ljava/io/InputStream;");
+ assert(MID_open);
+ MID_openFd = env->GetMethodID(cls, "openFd",
+ "(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;");
+ assert(MID_openFd);
+ MID_list = env->GetMethodID(cls, "list",
+ "(Ljava/lang/String;)[Ljava/lang/String;");
+ assert(MID_list);
+}
+
+AndroidAssetArchive::~AndroidAssetArchive() {
+ JNIEnv* env = JNU_GetEnv();
+ env->DeleteGlobalRef(_am);
+}
+
+bool AndroidAssetArchive::hasFile(const Common::String &name) {
+ JNIEnv* env = JNU_GetEnv();
+ jstring path = env->NewStringUTF(name.c_str());
+ jobject result = env->CallObjectMethod(_am, MID_open, path, ACCESS_UNKNOWN);
+ if (env->ExceptionCheck()) {
+ // Assume FileNotFoundException
+ //warning("Error while calling AssetManager->open(%s)", name.c_str());
+ //env->ExceptionDescribe();
+ env->ExceptionClear();
+ env->DeleteLocalRef(path);
+ return false;
+ }
+ env->DeleteLocalRef(result);
+ env->DeleteLocalRef(path);
+ return true;
+}
+
+int AndroidAssetArchive::listMembers(Common::ArchiveMemberList &member_list) {
+ JNIEnv* env = JNU_GetEnv();
+ Common::List<Common::String> dirlist;
+ dirlist.push_back("");
+
+ int count = 0;
+ while (!dirlist.empty()) {
+ const Common::String dir = dirlist.back();
+ dirlist.pop_back();
+
+ jstring jpath = env->NewStringUTF(dir.c_str());
+ jobjectArray jpathlist = static_cast<jobjectArray>(env->CallObjectMethod(_am, MID_list, jpath));
+ if (env->ExceptionCheck()) {
+ warning("Error while calling AssetManager->list(%s). Ignoring.",
+ dir.c_str());
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ continue; // May as well keep going ...
+ }
+ env->DeleteLocalRef(jpath);
+
+ for (jsize i = 0; i < env->GetArrayLength(jpathlist); ++i) {
+ jstring elem = (jstring)env->GetObjectArrayElement(jpathlist, i);
+ const char* p = env->GetStringUTFChars(elem, NULL);
+ Common::String thispath = dir;
+ if (!thispath.empty())
+ thispath += "/";
+ thispath += p;
+
+ // Assume files have a . in them, and directories don't
+ if (strchr(p, '.')) {
+ member_list.push_back(getMember(thispath));
+ ++count;
+ } else
+ dirlist.push_back(thispath);
+
+ env->ReleaseStringUTFChars(elem, p);
+ env->DeleteLocalRef(elem);
+ }
+
+ env->DeleteLocalRef(jpathlist);
+ }
+
+ return count;
+}
+
+Common::ArchiveMemberPtr AndroidAssetArchive::getMember(const Common::String &name) {
+ return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(name, this));
+}
+
+Common::SeekableReadStream *AndroidAssetArchive::createReadStreamForMember(const Common::String &path) const {
+ JNIEnv* env = JNU_GetEnv();
+ jstring jpath = env->NewStringUTF(path.c_str());
+
+ // Try openFd() first ...
+ jobject afd = env->CallObjectMethod(_am, MID_openFd, jpath);
+ if (env->ExceptionCheck())
+ env->ExceptionClear();
+ else if (afd != NULL) {
+ // success :)
+ env->DeleteLocalRef(jpath);
+ return new AssetFdReadStream(env, afd);
+ }
+
+ // ... and fallback to normal open() if that doesn't work
+ jobject is = env->CallObjectMethod(_am, MID_open, jpath, ACCESS_RANDOM);
+ if (env->ExceptionCheck()) {
+ // Assume FileNotFoundException
+ //warning("Error opening %s", path.c_str());
+ //env->ExceptionDescribe();
+ env->ExceptionClear();
+ env->DeleteLocalRef(jpath);
+ return NULL;
+ }
+
+ return new JavaInputStream(env, is);
+}
+
+#endif
diff --git a/backends/platform/android/asset-archive.h b/backends/platform/android/asset-archive.h
new file mode 100644
index 0000000000..b3f6993c50
--- /dev/null
+++ b/backends/platform/android/asset-archive.h
@@ -0,0 +1,53 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * $URL$
+ * $Id$
+ *
+ */
+
+#if defined(ANDROID)
+
+#include <jni.h>
+
+#include "common/str.h"
+#include "common/stream.h"
+#include "common/util.h"
+#include "common/archive.h"
+
+class AndroidAssetArchive : public Common::Archive {
+public:
+ AndroidAssetArchive(jobject am);
+ virtual ~AndroidAssetArchive();
+
+ virtual bool hasFile(const Common::String &name);
+ virtual int listMembers(Common::ArchiveMemberList &list);
+ virtual Common::ArchiveMemberPtr getMember(const Common::String &name);
+ virtual Common::SeekableReadStream *createReadStreamForMember(const Common::String &name) const;
+
+private:
+ jmethodID MID_open;
+ jmethodID MID_openFd;
+ jmethodID MID_list;
+
+ jobject _am;
+};
+
+#endif
diff --git a/backends/platform/android/module.mk b/backends/platform/android/module.mk
new file mode 100644
index 0000000000..fdb0ed2ac4
--- /dev/null
+++ b/backends/platform/android/module.mk
@@ -0,0 +1,85 @@
+MODULE := backends/platform/android
+
+MODULE_OBJS := \
+ android.o asset-archive.o video.o
+
+MODULE_DIRS += \
+ backends/platform/android/
+
+# We don't use the rules.mk here on purpose
+OBJS := $(addprefix $(MODULE)/, $(MODULE_OBJS)) $(OBJS)
+
+JAVA_SRC = \
+ $(MODULE)/org/inodes/gus/scummvm/ScummVM.java \
+ $(MODULE)/org/inodes/gus/scummvm/ScummVMApplication.java \
+ $(MODULE)/org/inodes/gus/scummvm/ScummVMActivity.java \
+ $(MODULE)/org/inodes/gus/scummvm/EditableSurfaceView.java \
+ $(MODULE)/org/inodes/gus/scummvm/Unpacker.java \
+ $(MODULE)/org/inodes/gus/scummvm/Manifest.java \
+ $(MODULE)/org/inodes/gus/scummvm/R.java
+
+JAVA_PLUGIN_SRC = \
+ $(MODULE)/org/inodes/gus/scummvm/PluginProvider.java
+
+RESOURCES = \
+ $(srcdir)/dists/android/res/values/strings.xml \
+ $(srcdir)/dists/android/res/layout/main.xml \
+ $(srcdir)/dists/android/res/layout/splash.xml \
+ $(srcdir)/dists/android/res/drawable/gradient.xml \
+ $(srcdir)/dists/android/res/drawable/scummvm.png \
+ $(srcdir)/dists/android/res/drawable/scummvm_big.png
+
+ASSETS = $(DIST_FILES_ENGINEDATA) $(DIST_FILES_THEMES)
+
+PLUGIN_RESOURCES = \
+ $(srcdir)/dists/android/res/values/strings.xml \
+ $(srcdir)/dists/android/res/drawable/scummvm.png
+
+# These must be incremented for each market upload
+#ANDROID_VERSIONCODE = 6 Specified in dists/android/AndroidManifest.xml.in
+ANDROID_PLUGIN_VERSIONCODE = 6
+
+# This library contains scummvm proper
+build.tmp/libscummvm.so: $(OBJS)
+ @$(MKDIR) -p $(@D)
+ $(CXX) $(PLUGIN_LDFLAGS) -shared $(LDFLAGS) -Wl,-soname,$(@F) -Wl,--no-undefined -o $@ $(PRE_OBJS_FLAGS) $(OBJS) $(POST_OBJS_FLAGS) $(LIBS)
+
+
+backends/platform/android/org/inodes/gus/scummvm/R.java backends/platform/android/org/inodes/gus/scummvm/Manifest.java: $(srcdir)/dists/android/AndroidManifest.xml $(filter %.xml,$(RESOURCES)) $(ANDROID_JAR)
+ $(AAPT) package -m -J backends/platform/android -M $< -S $(srcdir)/dists/android/res -I $(ANDROID_JAR)
+
+build.tmp/classes/%.class: $(srcdir)/backends/platform/android/%.java $(srcdir)/backends/platform/android/org/inodes/gus/scummvm/R.java
+ @$(MKDIR) -p $(@D)
+ $(JAVAC) $(JAVACFLAGS) -cp $(srcdir)/backends/platform/android -d build.tmp/classes -bootclasspath $(ANDROID_JAR) $<
+
+build.tmp/classes.plugin/%.class: $(srcdir)/backends/platform/android/%.java
+ @$(MKDIR) -p $(@D)
+ $(JAVAC) $(JAVACFLAGS) -cp $(srcdir)/backends/platform/android -d build.tmp/classes.plugin -bootclasspath $(ANDROID_JAR) $<
+
+classes.dex: $(JAVA_SRC:backends/platform/android/%.java=build.tmp/classes/%.class)
+ $(DX) --dex --output=$@ build.tmp/classes
+
+build.tmp/plugins/classes.dex: $(JAVA_PLUGIN_SRC:backends/platform/android/%.java=build.tmp/classes.plugin/%.class)
+ @$(MKDIR) -p $(@D)
+ $(DX) --dex --output=$@ build.tmp/classes.plugin
+
+resources.ap_: $(srcdir)/dists/android/AndroidManifest.xml $(RESOURCES) $(ASSETS) $(ANDROID_JAR) $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA)
+ $(INSTALL) -d build.tmp/assets/
+ $(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) build.tmp/assets/
+ $(AAPT) package -f -M $< -S $(srcdir)/dists/android/res -A build.tmp/assets -I $(ANDROID_JAR) -F $@
+
+build.tmp/%/resources.ap_: build.tmp/%/AndroidManifest.xml build.stage/%/res/values/strings.xml build.stage/%/res/drawable/scummvm.png $(ANDROID_JAR)
+ $(AAPT) package -f -M $< -S build.stage/$*/res -I $(ANDROID_JAR) -F $@
+
+build.tmp/%/AndroidManifest.xml build.stage/%/res/values/strings.xml: dists/android/mkmanifest.pl configure dists/android/AndroidManifest.xml
+ dists/android/mkmanifest.pl --id=$* --configure=configure \
+ --version-name=$(VERSION) \
+ --version-code=$(ANDROID_PLUGIN_VERSIONCODE) \
+ --stringres=build.stage/$*/res/values/strings.xml \
+ --manifest=build.tmp/$*/AndroidManifest.xml \
+ --master-manifest=dists/android/AndroidManifest.xml \
+ --unpacklib=mylib/armeabi/lib$*.so
+
+build.stage/%/res/drawable/scummvm.png: dists/android/res/drawable/scummvm.png
+ @$(MKDIR) -p $(@D)
+ $(CP) $< $@
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();
+ }
+ }
+}
diff --git a/backends/platform/android/scummvm-android-themeengine.patch b/backends/platform/android/scummvm-android-themeengine.patch
new file mode 100644
index 0000000000..1eafe7fb62
--- /dev/null
+++ b/backends/platform/android/scummvm-android-themeengine.patch
@@ -0,0 +1,135 @@
+diff -r 884e66fd1b9c gui/ThemeEngine.cpp
+--- a/gui/ThemeEngine.cpp Tue Apr 13 09:30:52 2010 +1000
++++ b/gui/ThemeEngine.cpp Fri May 28 23:24:43 2010 +1000
+@@ -390,21 +390,19 @@
+
+ // Try to create a Common::Archive with the files of the theme.
+ if (!_themeArchive && !_themeFile.empty()) {
+- Common::FSNode node(_themeFile);
+- if (node.getName().hasSuffix(".zip") && !node.isDirectory()) {
++ Common::ArchiveMemberPtr member = SearchMan.getMember(_themeFile);
++ if (member && member->getName().hasSuffix(".zip")) {
+ #ifdef USE_ZLIB
+- Common::Archive *zipArchive = Common::makeZipArchive(node);
++ Common::Archive *zipArchive = Common::makeZipArchive(member->createReadStream());
+
+ if (!zipArchive) {
+- warning("Failed to open Zip archive '%s'.", node.getPath().c_str());
++ warning("Failed to open Zip archive '%s'.", member->getDisplayName().c_str());
+ }
+ _themeArchive = zipArchive;
+ #else
+ warning("Trying to load theme '%s' in a Zip archive without zLib support", _themeFile.c_str());
+ return false;
+ #endif
+- } else if (node.isDirectory()) {
+- _themeArchive = new Common::FSDirectory(node);
+ }
+ }
+
+@@ -1436,6 +1434,30 @@
+ return tok.empty();
+ }
+
++bool ThemeEngine::themeConfigUsable(const Common::ArchiveMember &member, Common::String &themeName) {
++ Common::File stream;
++ bool foundHeader = false;
++
++ if (member.getName().hasSuffix(".zip")) {
++#ifdef USE_ZLIB
++ Common::Archive *zipArchive = Common::makeZipArchive(member.createReadStream());
++
++ if (zipArchive && zipArchive->hasFile("THEMERC")) {
++ stream.open("THEMERC", *zipArchive);
++ }
++
++ delete zipArchive;
++#endif
++ }
++
++ if (stream.isOpen()) {
++ Common::String stxHeader = stream.readLine();
++ foundHeader = themeConfigParseHeader(stxHeader, themeName);
++ }
++
++ return foundHeader;
++}
++
+ bool ThemeEngine::themeConfigUsable(const Common::FSNode &node, Common::String &themeName) {
+ Common::File stream;
+ bool foundHeader = false;
+@@ -1493,10 +1515,6 @@
+ if (ConfMan.hasKey("themepath"))
+ listUsableThemes(Common::FSNode(ConfMan.get("themepath")), list);
+
+-#ifdef DATA_PATH
+- listUsableThemes(Common::FSNode(DATA_PATH), list);
+-#endif
+-
+ #if defined(MACOSX) || defined(IPHONE)
+ CFURLRef resourceUrl = CFBundleCopyResourcesDirectoryURL(CFBundleGetMainBundle());
+ if (resourceUrl) {
+@@ -1509,10 +1527,7 @@
+ }
+ #endif
+
+- if (ConfMan.hasKey("extrapath"))
+- listUsableThemes(Common::FSNode(ConfMan.get("extrapath")), list);
+-
+- listUsableThemes(Common::FSNode("."), list, 1);
++ listUsableThemes(SearchMan, list);
+
+ // Now we need to strip all duplicates
+ // TODO: It might not be the best idea to strip duplicates. The user might
+@@ -1531,6 +1546,34 @@
+ output.clear();
+ }
+
++void ThemeEngine::listUsableThemes(Common::Archive &archive, Common::List<ThemeDescriptor> &list) {
++ ThemeDescriptor td;
++
++#ifdef USE_ZLIB
++ Common::ArchiveMemberList fileList;
++ archive.listMatchingMembers(fileList, "*.zip");
++ for (Common::ArchiveMemberList::iterator i = fileList.begin();
++ i != fileList.end(); ++i) {
++ td.name.clear();
++ if (themeConfigUsable(**i, td.name)) {
++ td.filename = (*i)->getName();
++ td.id = (*i)->getDisplayName();
++
++ // If the name of the node object also contains
++ // the ".zip" suffix, we will strip it.
++ if (td.id.hasSuffix(".zip")) {
++ for (int j = 0; j < 4; ++j)
++ td.id.deleteLastChar();
++ }
++
++ list.push_back(td);
++ }
++ }
++
++ fileList.clear();
++#endif
++}
++
+ void ThemeEngine::listUsableThemes(const Common::FSNode &node, Common::List<ThemeDescriptor> &list, int depth) {
+ if (!node.exists() || !node.isReadable() || !node.isDirectory())
+ return;
+diff -r 884e66fd1b9c gui/ThemeEngine.h
+--- a/gui/ThemeEngine.h Tue Apr 13 09:30:52 2010 +1000
++++ b/gui/ThemeEngine.h Fri May 28 23:24:43 2010 +1000
+@@ -560,11 +560,13 @@
+ static void listUsableThemes(Common::List<ThemeDescriptor> &list);
+ private:
+ static bool themeConfigUsable(const Common::FSNode &node, Common::String &themeName);
++ static bool themeConfigUsable(const Common::ArchiveMember &member, Common::String &themeName);
+ static bool themeConfigParseHeader(Common::String header, Common::String &themeName);
+
+ static Common::String getThemeFile(const Common::String &id);
+ static Common::String getThemeId(const Common::String &filename);
+ static void listUsableThemes(const Common::FSNode &node, Common::List<ThemeDescriptor> &list, int depth = -1);
++ static void listUsableThemes(Common::Archive &archive, Common::List<ThemeDescriptor> &list);
+
+ protected:
+ OSystem *_system; /** Global system object. */
diff --git a/base/commandLine.cpp b/base/commandLine.cpp
index 207ff79c4c..1c548d3f50 100644
--- a/base/commandLine.cpp
+++ b/base/commandLine.cpp
@@ -51,7 +51,7 @@ static const char USAGE_STRING[] =
;
// DONT FIXME: DO NOT ORDER ALPHABETICALLY, THIS IS ORDERED BY IMPORTANCE/CATEGORY! :)
-#if defined(PALMOS_MODE) || defined(__SYMBIAN32__) || defined(__GP32__)
+#if defined(PALMOS_MODE) || defined(__SYMBIAN32__) || defined(__GP32__) || defined(ANDROID)
static const char HELP_STRING[] = "NoUsageString"; // save more data segment space
#else
static const char HELP_STRING[] =
@@ -948,7 +948,7 @@ Common::Error processSettings(Common::String &command, Common::StringMap &settin
// environment variable. This is weaker than a --savepath on the
// command line, but overrides the default savepath, hence it is
// handled here, just before the command line gets parsed.
-#if !defined(MACOS_CARBON) && !defined(_WIN32_WCE) && !defined(PALMOS_MODE) && !defined(__GP32__)
+#if !defined(MACOS_CARBON) && !defined(_WIN32_WCE) && !defined(PALMOS_MODE) && !defined(__GP32__) && !defined(ANDROID)
if (!settings.contains("savepath")) {
const char *dir = getenv("SCUMMVM_SAVEPATH");
if (dir && *dir && strlen(dir) < MAXPATHLEN) {
diff --git a/common/textconsole.cpp b/common/textconsole.cpp
index eef58fa39c..87ba55ebf1 100644
--- a/common/textconsole.cpp
+++ b/common/textconsole.cpp
@@ -43,6 +43,10 @@ extern bool isSmartphone();
#define fputs(str, file) DS::std_fwrite(str, strlen(str), 1, file)
#endif
+#ifdef ANDROID
+ #include <android/log.h>
+#endif
+
namespace Common {
static OutputFormatter s_errorOutputFormatter = 0;
@@ -71,7 +75,9 @@ void warning(const char *s, ...) {
vsnprintf(buf, STRINGBUFLEN, s, va);
va_end(va);
-#if !defined (__SYMBIAN32__)
+#if defined( ANDROID )
+ __android_log_write(ANDROID_LOG_WARN, "ScummVM", buf);
+#elif !defined (__SYMBIAN32__)
fputs("WARNING: ", stderr);
fputs(buf, stderr);
fputs("!\n", stderr);
@@ -141,6 +147,10 @@ void NORETURN_PRE error(const char *s, ...) {
#endif
#endif
+#ifdef ANDROID
+ __android_log_assert("Fatal error", "ScummVM", "%s", buf_output);
+#endif
+
#ifdef PALMOS_MODE
extern void PalmFatalError(const char *err);
PalmFatalError(buf_output);
diff --git a/configure b/configure
index 17da22fb8f..0c06be13ea 100755
--- a/configure
+++ b/configure
@@ -999,6 +999,11 @@ wince)
_host_cpu=arm
_host_alias=arm-wince-mingw32ce
;;
+android)
+ _host_os=android
+ _host_cpu=arm
+ _host_alias=arm-android-eabi
+ ;;
*)
if test -n "$_host"; then
guessed_host=`$_srcdir/config.sub $_host`
@@ -1077,6 +1082,12 @@ psp)
exit 1
fi
;;
+android)
+ if test -z "$ANDROID_SDK"; then
+ echo "Please set ANDROID_SDK in your environment. export ANDROID_SDK=<path to Android SDK>"
+ exit 1
+ fi
+ ;;
*)
;;
esac
@@ -1399,6 +1410,11 @@ case $_host_os in
DEFINES="$DEFINES -D_WIN32_WCE=300 -D__ARM__ -D_ARM_ -DUNICODE -DFPM_DEFAULT -DNONSTANDARD_PORT"
DEFINES="$DEFINES -DWIN32 -Dcdecl= -D__cdecl__="
;;
+ android)
+ DEFINES="$DEFINES -DUNIX"
+ CXXFLAGS="$CXXFLAGS -Os -msoft-float -mtune=xscale -march=armv5te -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5TE__"
+ add_line_to_config_mk "ANDROID_SDK = $ANDROID_SDK"
+ ;;
# given this is a shell script assume some type of unix
*)
echo "WARNING: could not establish system type, assuming unix like"
@@ -1647,6 +1663,19 @@ if test -n "$_host"; then
_mt32emu="no"
_port_mk="backends/platform/wince/wince.mk"
;;
+ android)
+ DEFINES="$DEFINES -DANDROID -DUNIX -DUSE_ARM_SMUSH_ASM"
+ _endian=little
+ _need_memalign=yes
+ add_line_to_config_mk 'USE_ARM_SOUND_ASM = 1'
+ add_line_to_config_mk 'USE_ARM_SMUSH_ASM = 1'
+ add_line_to_config_mk 'USE_ARM_GFX_ASM = 1'
+ add_line_to_config_mk 'USE_ARM_SCALER_ASM = 1'
+ add_line_to_config_mk 'USE_ARM_COSTUME_ASM = 1'
+ _backend="android"
+ _port_mk="backends/platform/android/android.mk"
+ _build_hq_scalers="no"
+ ;;
*)
echo "WARNING: Unknown target, continuing with auto-detected values"
;;
@@ -1825,7 +1854,7 @@ POST_OBJS_FLAGS := -Wl,-no-whole-archive
LIBS += -ldl
'
;;
- linux*)
+ linux*|android)
_def_plugin='
#define PLUGIN_PREFIX "lib"
#define PLUGIN_SUFFIX ".so"
@@ -2432,6 +2461,14 @@ case $_backend in
INCLUDES="$INCLUDES "'-I$(srcdir) -I$(srcdir)/backends/platform/wince -I$(srcdir)/engines -I$(srcdir)/backends/platform/wince/missing/gcc -I$(srcdir)/backends/platform/wince/CEgui -I$(srcdir)/backends/platform/wince/CEkeys'
LIBS="$LIBS -static -lSDL"
;;
+ android)
+ # -lgcc is carefully placed here - we want to catch
+ # all toolchain symbols in *our* libraries rather
+ # than pick up anything unhygenic from the Android libs.
+ LIBS="$LIBS -lgcc -lstdc++ -llog -lGLESv1_CM -lEGL"
+ DEFINES="$DEFINES -D__ANDROID__ -DANDROID_BACKEND -DREDUCE_MEMORY_USAGE"
+ add_line_to_config_mk 'PLUGIN_LDFLAGS += $(LDFLAGS) -Wl,-shared,-Bsymbolic'
+ ;;
*)
echo "support for $_backend backend not implemented in configure script yet"
exit 1
@@ -2447,7 +2484,7 @@ if test "$have_gcc" = yes ; then
case $_host_os in
# newlib-based system include files suppress non-C89 function
# declarations under __STRICT_ANSI__
- mingw* | dreamcast | wii | gamecube | psp | wince | amigaos*)
+ mingw* | dreamcast | wii | gamecube | psp | wince | amigaos* | android)
CXXFLAGS="$CXXFLAGS -W -Wno-unused-parameter"
;;
*)
@@ -2468,7 +2505,7 @@ fi;
# Some platforms use certain GNU extensions in header files
case $_host_os in
-gamecube | psp | wii)
+gamecube | psp | wii | android)
;;
*)
CXXFLAGS="$CXXFLAGS -pedantic"
diff --git a/dists/android/mkmanifest.pl b/dists/android/mkmanifest.pl
new file mode 100644
index 0000000000..00d15f561e
--- /dev/null
+++ b/dists/android/mkmanifest.pl
@@ -0,0 +1,169 @@
+#!/usr/bin/perl
+
+use File::Basename qw(dirname);
+use File::Path qw(mkpath);
+use IO::File;
+use XML::Writer;
+use XML::Parser;
+use Getopt::Long;
+
+use warnings;
+use strict;
+
+use constant ANDROID => 'http://schemas.android.com/apk/res/android';
+
+my $id;
+my $package_versionName;
+my $package_versionCode;
+my $configure = 'configure';
+my $stringres = 'res/string/values.xml';
+my $manifest = 'AndroidManifest.xml';
+my $master_manifest;
+my @unpack_libs;
+GetOptions('id=s' => \$id,
+ 'version-name=s' => \$package_versionName,
+ 'version-code=i' => \$package_versionCode,
+ 'configure=s' => \$configure,
+ 'stringres=s' => \$stringres,
+ 'manifest=s' => \$manifest,
+ 'master-manifest=s' => \$master_manifest,
+ 'unpacklib=s' => \@unpack_libs,
+ ) or die;
+die "Missing required arg"
+ unless $id and $package_versionName and $package_versionCode;
+
+
+sub grope_engine_info {
+ my $configure = shift;
+ my @ret;
+ while (<$configure>) {
+ m/^add_engine \s+ (\w+) \s+ "(.*?)" \s+ \w+ (?:\s+ "([\w\s]*)")?/x
+ or next;
+ my $subengines = $3 || '';
+ my %info = (id => $1, name => $2,
+ subengines => [split / /, $subengines]);
+ push @ret, \%info;
+ }
+ return @ret;
+}
+
+sub read_constraints {
+ my $manifest = shift;
+ my @constraints;
+ my $parser = new XML::Parser Handlers => {
+ Start => sub {
+ my $expat = shift;
+ my $elem = shift;
+ return if $elem !~
+ /^(uses-configuration|supports-screens|uses-sdk)$/;
+ my @constraint = ($elem);
+ while (@_) {
+ my $attr = shift;
+ my $value = shift;
+ $attr = [ANDROID, $attr] if $attr =~ s/^android://;
+ push @constraint, $attr, $value;
+ }
+ push @constraints, \@constraint;
+ },
+ };
+ $parser->parse($manifest);
+ return @constraints;
+}
+
+sub print_stringres {
+ my $output = shift;
+ my $info = shift;
+
+ my $writer = new XML::Writer(OUTPUT => $output, ENCODING => 'utf-8',
+ DATA_MODE => 1, DATA_INDENT => 2);
+
+ $writer->xmlDecl();
+ $writer->startTag('resources');
+
+ while (my ($k,$v) = each %$info) {
+ $writer->dataElement('string', $v, name => $k);
+ }
+
+ $writer->endTag('resources');
+ $writer->end();
+}
+
+sub print_manifest {
+ my $output = shift;
+ my $info = shift;
+ my $constraints = shift;
+
+ my $writer = new XML::Writer(OUTPUT => $output, ENCODING => 'utf-8',
+ DATA_MODE => 1, DATA_INDENT => 2,
+ NAMESPACES => 1,
+ PREFIX_MAP => {ANDROID, 'android'});
+
+ $writer->xmlDecl();
+
+ $writer->startTag(
+ 'manifest',
+ 'package' => "org.inodes.gus.scummvm.plugin.$info->{name}",
+ [ANDROID, 'versionCode'] => $package_versionCode,
+ [ANDROID, 'versionName'] => $package_versionName,
+ );
+
+ $writer->startTag(
+ 'application',
+ [ANDROID, 'label'] => '@string/app_name',
+ [ANDROID, 'description'] => '@string/app_desc',
+ [ANDROID, 'icon'] => '@drawable/scummvm',
+ );
+
+ $writer->startTag(
+ 'receiver',
+ [ANDROID, 'name'] => 'org.inodes.gus.scummvm.PluginProvider',
+ [ANDROID, 'process'] => 'org.inodes.gus.scummvm');
+
+ $writer->startTag('intent-filter');
+ $writer->emptyTag('action', [ANDROID, 'name'] =>
+ 'org.inodes.gus.scummvm.action.PLUGIN_QUERY');
+ $writer->emptyTag('category', [ANDROID, 'name'] =>
+ 'android.intent.category.INFO');
+ $writer->endTag('intent-filter');
+ $writer->emptyTag(
+ 'meta-data',
+ [ANDROID, 'name'] => 'org.inodes.gus.scummvm.meta.UNPACK_LIB',
+ [ANDROID, 'value'] => $_)
+ for @{$info->{unpack_libs}};
+
+ $writer->endTag('receiver');
+ $writer->endTag('application');
+
+ $writer->emptyTag('uses-permission', [ANDROID, 'name'] =>
+ 'org.inodes.gus.scummvm.permission.SCUMMVM_PLUGIN');
+
+ $writer->emptyTag(@$_) foreach @$constraints;
+
+ $writer->endTag('manifest');
+ $writer->end();
+}
+
+
+my %engines;
+for my $engine (grope_engine_info(new IO::File $configure, 'r')) {
+ $engines{$engine->{id}} = $engine;
+}
+
+my @games = ($id, @{$engines{$id}{subengines}});
+my $games_desc = join('; ', map $engines{$_}{name}, @games);
+
+my @constraints = read_constraints(new IO::File $master_manifest, 'r');
+
+print "Writing $stringres ...\n";
+mkpath(dirname($stringres));
+print_stringres(IO::File->new($stringres, 'w'),
+ {app_name => qq{ScummVM plugin: "$id"},
+ app_desc => "Game engine for: $games_desc",
+ });
+
+print "Writing $manifest ...\n";
+mkpath(dirname($manifest));
+print_manifest(IO::File->new($manifest, 'w'),
+ {name => $id, unpack_libs => \@unpack_libs}, \@constraints);
+
+exit 0;
diff --git a/dists/android/res/drawable/gradient.xml b/dists/android/res/drawable/gradient.xml
new file mode 100644
index 0000000000..dbfd9b5b34
--- /dev/null
+++ b/dists/android/res/drawable/gradient.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:startColor="#e9bb8b"
+ android:endColor="#d16e09"
+ android:angle="315" />
+</shape>
diff --git a/dists/android/res/layout/main.xml b/dists/android/res/layout/main.xml
new file mode 100644
index 0000000000..f5276ce41b
--- /dev/null
+++ b/dists/android/res/layout/main.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.inodes.gus.scummvm.EditableSurfaceView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent" android:layout_height="fill_parent"
+ android:id="@+id/main_surface"
+ android:gravity="center"
+ android:keepScreenOn="true"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+/>
diff --git a/dists/android/res/layout/splash.xml b/dists/android/res/layout/splash.xml
new file mode 100644
index 0000000000..e9fd5f70e7
--- /dev/null
+++ b/dists/android/res/layout/splash.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:background="@drawable/gradient"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:scaleType="fitCenter"
+ android:src="@drawable/scummvm_big" />
+ <ProgressBar android:id="@+id/progress"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="300dip"
+ android:layout_height="wrap_content"
+ android:padding="20dip"/>
+</LinearLayout>
diff --git a/dists/android/res/values/strings.xml b/dists/android/res/values/strings.xml
new file mode 100644
index 0000000000..e06509d3ed
--- /dev/null
+++ b/dists/android/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">ScummVM</string>
+ <string name="app_desc">Graphic adventure game engine</string>
+ <string name="quit">Quit</string>
+ <string name="scummvm_perm_plugin_label">ScummVM plugin</string>
+ <string name="scummvm_perm_plugin_desc">Allows the application to
+ provide a ScummVM loadable plugin: code that will be executed in the
+ ScummVM application. Malicious plugins may do anything ScummVM
+ itself could do: write to your SD card, delete your savegames,
+ change the ScummVM background to puce, replace menu labels with rude
+ words, etc.</string>
+ <string name="no_sdcard_title">No SD card?</string>
+ <string name="no_sdcard">Unable to read your SD card. This usually
+ means you still have it mounted on your PC. Unmount, reinsert,
+ whatever and then try again.</string>
+ <string name="no_plugins_title">No plugins found</string>
+ <string name="no_plugins_found">ScummVM requires at least one <i>game
+ engine</i> to be useful. Engines are available as separate plugin
+ packages, from wherever you found ScummVM.</string>
+ <string name="to_market">To Market</string>
+</resources>
diff --git a/sound/decoders/vorbis.cpp b/sound/decoders/vorbis.cpp
index 39068603de..64869d7843 100644
--- a/sound/decoders/vorbis.cpp
+++ b/sound/decoders/vorbis.cpp
@@ -35,7 +35,7 @@
#include "sound/audiocd.h"
#ifdef USE_TREMOR
-#ifdef __GP32__ // GP32 uses custom libtremor
+#if defined(ANDROID) || defined(__GP32__) // custom libtremor locations
#include <ivorbisfile.h>
#else
#include <tremor/ivorbisfile.h>
diff --git a/tools/update-version.pl b/tools/update-version.pl
index f151ae7514..81aa5c27f9 100755
--- a/tools/update-version.pl
+++ b/tools/update-version.pl
@@ -40,6 +40,7 @@ my @subs_files = qw(
dists/iphone/Info.plist
dists/irix/scummvm.spec
dists/wii/meta.xml
+ dists/android/AndroidManifest.xml
backends/platform/psp/README.PSP
);