diff options
author | Max Horn | 2010-06-06 09:34:36 +0000 |
---|---|---|
committer | Max Horn | 2010-06-06 09:34:36 +0000 |
commit | 46155b2c3678784f6333eed1d65a35eefdcb2001 (patch) | |
tree | 1f570683935a5bede0e2475493a4f48b1548d320 /backends/platform/android | |
parent | 3efec5720de2c46355c323763dee96b719ed5aa1 (diff) | |
download | scummvm-rg350-46155b2c3678784f6333eed1d65a35eefdcb2001.tar.gz scummvm-rg350-46155b2c3678784f6333eed1d65a35eefdcb2001.tar.bz2 scummvm-rg350-46155b2c3678784f6333eed1d65a35eefdcb2001.zip |
Add Android backend from patch #2603856
svn-id: r49449
Diffstat (limited to 'backends/platform/android')
-rw-r--r-- | backends/platform/android/README.build | 84 | ||||
-rw-r--r-- | backends/platform/android/android.cpp | 1413 | ||||
-rw-r--r-- | backends/platform/android/android.mk | 52 | ||||
-rw-r--r-- | backends/platform/android/asset-archive.cpp | 414 | ||||
-rw-r--r-- | backends/platform/android/asset-archive.h | 53 | ||||
-rw-r--r-- | backends/platform/android/module.mk | 85 | ||||
-rw-r--r-- | backends/platform/android/org/inodes/gus/scummvm/EditableSurfaceView.java | 59 | ||||
-rw-r--r-- | backends/platform/android/org/inodes/gus/scummvm/Event.java | 330 | ||||
-rw-r--r-- | backends/platform/android/org/inodes/gus/scummvm/PluginProvider.java | 52 | ||||
-rw-r--r-- | backends/platform/android/org/inodes/gus/scummvm/ScummVM.java | 317 | ||||
-rw-r--r-- | backends/platform/android/org/inodes/gus/scummvm/ScummVMActivity.java | 446 | ||||
-rw-r--r-- | backends/platform/android/org/inodes/gus/scummvm/ScummVMApplication.java | 29 | ||||
-rw-r--r-- | backends/platform/android/org/inodes/gus/scummvm/Unpacker.java | 370 | ||||
-rw-r--r-- | backends/platform/android/scummvm-android-themeengine.patch | 135 |
14 files changed, 3839 insertions, 0 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. */ |