diff options
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..a6258df554 --- /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..840f3440d5 --- /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..f4dca0e7e5 --- /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..fb3cd6348f --- /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. */  | 
