Add Android backend from patch #2603856

svn-id: r49449
This commit is contained in:
Max Horn 2010-06-06 09:34:36 +00:00
parent 3efec5720d
commit 46155b2c36
24 changed files with 4121 additions and 7 deletions

View file

@ -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.

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) $< $@

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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. */

View file

@ -51,7 +51,7 @@ static const char USAGE_STRING[] =
;
// DONT FIXME: DO NOT ORDER ALPHABETICALLY, THIS IS ORDERED BY IMPORTANCE/CATEGORY! :)
#if defined(PALMOS_MODE) || defined(__SYMBIAN32__) || defined(__GP32__)
#if defined(PALMOS_MODE) || defined(__SYMBIAN32__) || defined(__GP32__) || defined(ANDROID)
static const char HELP_STRING[] = "NoUsageString"; // save more data segment space
#else
static const char HELP_STRING[] =
@ -948,7 +948,7 @@ Common::Error processSettings(Common::String &command, Common::StringMap &settin
// environment variable. This is weaker than a --savepath on the
// command line, but overrides the default savepath, hence it is
// handled here, just before the command line gets parsed.
#if !defined(MACOS_CARBON) && !defined(_WIN32_WCE) && !defined(PALMOS_MODE) && !defined(__GP32__)
#if !defined(MACOS_CARBON) && !defined(_WIN32_WCE) && !defined(PALMOS_MODE) && !defined(__GP32__) && !defined(ANDROID)
if (!settings.contains("savepath")) {
const char *dir = getenv("SCUMMVM_SAVEPATH");
if (dir && *dir && strlen(dir) < MAXPATHLEN) {

View file

@ -43,6 +43,10 @@ extern bool isSmartphone();
#define fputs(str, file) DS::std_fwrite(str, strlen(str), 1, file)
#endif
#ifdef ANDROID
#include <android/log.h>
#endif
namespace Common {
static OutputFormatter s_errorOutputFormatter = 0;
@ -71,7 +75,9 @@ void warning(const char *s, ...) {
vsnprintf(buf, STRINGBUFLEN, s, va);
va_end(va);
#if !defined (__SYMBIAN32__)
#if defined( ANDROID )
__android_log_write(ANDROID_LOG_WARN, "ScummVM", buf);
#elif !defined (__SYMBIAN32__)
fputs("WARNING: ", stderr);
fputs(buf, stderr);
fputs("!\n", stderr);
@ -141,6 +147,10 @@ void NORETURN_PRE error(const char *s, ...) {
#endif
#endif
#ifdef ANDROID
__android_log_assert("Fatal error", "ScummVM", "%s", buf_output);
#endif
#ifdef PALMOS_MODE
extern void PalmFatalError(const char *err);
PalmFatalError(buf_output);

43
configure vendored
View file

@ -999,6 +999,11 @@ wince)
_host_cpu=arm
_host_alias=arm-wince-mingw32ce
;;
android)
_host_os=android
_host_cpu=arm
_host_alias=arm-android-eabi
;;
*)
if test -n "$_host"; then
guessed_host=`$_srcdir/config.sub $_host`
@ -1077,6 +1082,12 @@ psp)
exit 1
fi
;;
android)
if test -z "$ANDROID_SDK"; then
echo "Please set ANDROID_SDK in your environment. export ANDROID_SDK=<path to Android SDK>"
exit 1
fi
;;
*)
;;
esac
@ -1399,6 +1410,11 @@ case $_host_os in
DEFINES="$DEFINES -D_WIN32_WCE=300 -D__ARM__ -D_ARM_ -DUNICODE -DFPM_DEFAULT -DNONSTANDARD_PORT"
DEFINES="$DEFINES -DWIN32 -Dcdecl= -D__cdecl__="
;;
android)
DEFINES="$DEFINES -DUNIX"
CXXFLAGS="$CXXFLAGS -Os -msoft-float -mtune=xscale -march=armv5te -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5TE__"
add_line_to_config_mk "ANDROID_SDK = $ANDROID_SDK"
;;
# given this is a shell script assume some type of unix
*)
echo "WARNING: could not establish system type, assuming unix like"
@ -1647,6 +1663,19 @@ if test -n "$_host"; then
_mt32emu="no"
_port_mk="backends/platform/wince/wince.mk"
;;
android)
DEFINES="$DEFINES -DANDROID -DUNIX -DUSE_ARM_SMUSH_ASM"
_endian=little
_need_memalign=yes
add_line_to_config_mk 'USE_ARM_SOUND_ASM = 1'
add_line_to_config_mk 'USE_ARM_SMUSH_ASM = 1'
add_line_to_config_mk 'USE_ARM_GFX_ASM = 1'
add_line_to_config_mk 'USE_ARM_SCALER_ASM = 1'
add_line_to_config_mk 'USE_ARM_COSTUME_ASM = 1'
_backend="android"
_port_mk="backends/platform/android/android.mk"
_build_hq_scalers="no"
;;
*)
echo "WARNING: Unknown target, continuing with auto-detected values"
;;
@ -1825,7 +1854,7 @@ POST_OBJS_FLAGS := -Wl,-no-whole-archive
LIBS += -ldl
'
;;
linux*)
linux*|android)
_def_plugin='
#define PLUGIN_PREFIX "lib"
#define PLUGIN_SUFFIX ".so"
@ -2432,6 +2461,14 @@ case $_backend in
INCLUDES="$INCLUDES "'-I$(srcdir) -I$(srcdir)/backends/platform/wince -I$(srcdir)/engines -I$(srcdir)/backends/platform/wince/missing/gcc -I$(srcdir)/backends/platform/wince/CEgui -I$(srcdir)/backends/platform/wince/CEkeys'
LIBS="$LIBS -static -lSDL"
;;
android)
# -lgcc is carefully placed here - we want to catch
# all toolchain symbols in *our* libraries rather
# than pick up anything unhygenic from the Android libs.
LIBS="$LIBS -lgcc -lstdc++ -llog -lGLESv1_CM -lEGL"
DEFINES="$DEFINES -D__ANDROID__ -DANDROID_BACKEND -DREDUCE_MEMORY_USAGE"
add_line_to_config_mk 'PLUGIN_LDFLAGS += $(LDFLAGS) -Wl,-shared,-Bsymbolic'
;;
*)
echo "support for $_backend backend not implemented in configure script yet"
exit 1
@ -2447,7 +2484,7 @@ if test "$have_gcc" = yes ; then
case $_host_os in
# newlib-based system include files suppress non-C89 function
# declarations under __STRICT_ANSI__
mingw* | dreamcast | wii | gamecube | psp | wince | amigaos*)
mingw* | dreamcast | wii | gamecube | psp | wince | amigaos* | android)
CXXFLAGS="$CXXFLAGS -W -Wno-unused-parameter"
;;
*)
@ -2468,7 +2505,7 @@ fi;
# Some platforms use certain GNU extensions in header files
case $_host_os in
gamecube | psp | wii)
gamecube | psp | wii | android)
;;
*)
CXXFLAGS="$CXXFLAGS -pedantic"

169
dists/android/mkmanifest.pl Normal file
View file

@ -0,0 +1,169 @@
#!/usr/bin/perl
use File::Basename qw(dirname);
use File::Path qw(mkpath);
use IO::File;
use XML::Writer;
use XML::Parser;
use Getopt::Long;
use warnings;
use strict;
use constant ANDROID => 'http://schemas.android.com/apk/res/android';
my $id;
my $package_versionName;
my $package_versionCode;
my $configure = 'configure';
my $stringres = 'res/string/values.xml';
my $manifest = 'AndroidManifest.xml';
my $master_manifest;
my @unpack_libs;
GetOptions('id=s' => \$id,
'version-name=s' => \$package_versionName,
'version-code=i' => \$package_versionCode,
'configure=s' => \$configure,
'stringres=s' => \$stringres,
'manifest=s' => \$manifest,
'master-manifest=s' => \$master_manifest,
'unpacklib=s' => \@unpack_libs,
) or die;
die "Missing required arg"
unless $id and $package_versionName and $package_versionCode;
sub grope_engine_info {
my $configure = shift;
my @ret;
while (<$configure>) {
m/^add_engine \s+ (\w+) \s+ "(.*?)" \s+ \w+ (?:\s+ "([\w\s]*)")?/x
or next;
my $subengines = $3 || '';
my %info = (id => $1, name => $2,
subengines => [split / /, $subengines]);
push @ret, \%info;
}
return @ret;
}
sub read_constraints {
my $manifest = shift;
my @constraints;
my $parser = new XML::Parser Handlers => {
Start => sub {
my $expat = shift;
my $elem = shift;
return if $elem !~
/^(uses-configuration|supports-screens|uses-sdk)$/;
my @constraint = ($elem);
while (@_) {
my $attr = shift;
my $value = shift;
$attr = [ANDROID, $attr] if $attr =~ s/^android://;
push @constraint, $attr, $value;
}
push @constraints, \@constraint;
},
};
$parser->parse($manifest);
return @constraints;
}
sub print_stringres {
my $output = shift;
my $info = shift;
my $writer = new XML::Writer(OUTPUT => $output, ENCODING => 'utf-8',
DATA_MODE => 1, DATA_INDENT => 2);
$writer->xmlDecl();
$writer->startTag('resources');
while (my ($k,$v) = each %$info) {
$writer->dataElement('string', $v, name => $k);
}
$writer->endTag('resources');
$writer->end();
}
sub print_manifest {
my $output = shift;
my $info = shift;
my $constraints = shift;
my $writer = new XML::Writer(OUTPUT => $output, ENCODING => 'utf-8',
DATA_MODE => 1, DATA_INDENT => 2,
NAMESPACES => 1,
PREFIX_MAP => {ANDROID, 'android'});
$writer->xmlDecl();
$writer->startTag(
'manifest',
'package' => "org.inodes.gus.scummvm.plugin.$info->{name}",
[ANDROID, 'versionCode'] => $package_versionCode,
[ANDROID, 'versionName'] => $package_versionName,
);
$writer->startTag(
'application',
[ANDROID, 'label'] => '@string/app_name',
[ANDROID, 'description'] => '@string/app_desc',
[ANDROID, 'icon'] => '@drawable/scummvm',
);
$writer->startTag(
'receiver',
[ANDROID, 'name'] => 'org.inodes.gus.scummvm.PluginProvider',
[ANDROID, 'process'] => 'org.inodes.gus.scummvm');
$writer->startTag('intent-filter');
$writer->emptyTag('action', [ANDROID, 'name'] =>
'org.inodes.gus.scummvm.action.PLUGIN_QUERY');
$writer->emptyTag('category', [ANDROID, 'name'] =>
'android.intent.category.INFO');
$writer->endTag('intent-filter');
$writer->emptyTag(
'meta-data',
[ANDROID, 'name'] => 'org.inodes.gus.scummvm.meta.UNPACK_LIB',
[ANDROID, 'value'] => $_)
for @{$info->{unpack_libs}};
$writer->endTag('receiver');
$writer->endTag('application');
$writer->emptyTag('uses-permission', [ANDROID, 'name'] =>
'org.inodes.gus.scummvm.permission.SCUMMVM_PLUGIN');
$writer->emptyTag(@$_) foreach @$constraints;
$writer->endTag('manifest');
$writer->end();
}
my %engines;
for my $engine (grope_engine_info(new IO::File $configure, 'r')) {
$engines{$engine->{id}} = $engine;
}
my @games = ($id, @{$engines{$id}{subengines}});
my $games_desc = join('; ', map $engines{$_}{name}, @games);
my @constraints = read_constraints(new IO::File $master_manifest, 'r');
print "Writing $stringres ...\n";
mkpath(dirname($stringres));
print_stringres(IO::File->new($stringres, 'w'),
{app_name => qq{ScummVM plugin: "$id"},
app_desc => "Game engine for: $games_desc",
});
print "Writing $manifest ...\n";
mkpath(dirname($manifest));
print_manifest(IO::File->new($manifest, 'w'),
{name => $id, unpack_libs => \@unpack_libs}, \@constraints);
exit 0;

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#e9bb8b"
android:endColor="#d16e09"
android:angle="315" />
</shape>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<org.inodes.gus.scummvm.EditableSurfaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:id="@+id/main_surface"
android:gravity="center"
android:keepScreenOn="true"
android:focusable="true"
android:focusableInTouchMode="true"
/>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:background="@drawable/gradient"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/scummvm_big" />
<ProgressBar android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="300dip"
android:layout_height="wrap_content"
android:padding="20dip"/>
</LinearLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ScummVM</string>
<string name="app_desc">Graphic adventure game engine</string>
<string name="quit">Quit</string>
<string name="scummvm_perm_plugin_label">ScummVM plugin</string>
<string name="scummvm_perm_plugin_desc">Allows the application to
provide a ScummVM loadable plugin: code that will be executed in the
ScummVM application. Malicious plugins may do anything ScummVM
itself could do: write to your SD card, delete your savegames,
change the ScummVM background to puce, replace menu labels with rude
words, etc.</string>
<string name="no_sdcard_title">No SD card?</string>
<string name="no_sdcard">Unable to read your SD card. This usually
means you still have it mounted on your PC. Unmount, reinsert,
whatever and then try again.</string>
<string name="no_plugins_title">No plugins found</string>
<string name="no_plugins_found">ScummVM requires at least one <i>game
engine</i> to be useful. Engines are available as separate plugin
packages, from wherever you found ScummVM.</string>
<string name="to_market">To Market</string>
</resources>

View file

@ -35,7 +35,7 @@
#include "sound/audiocd.h"
#ifdef USE_TREMOR
#ifdef __GP32__ // GP32 uses custom libtremor
#if defined(ANDROID) || defined(__GP32__) // custom libtremor locations
#include <ivorbisfile.h>
#else
#include <tremor/ivorbisfile.h>

View file

@ -40,6 +40,7 @@ my @subs_files = qw(
dists/iphone/Info.plist
dists/irix/scummvm.spec
dists/wii/meta.xml
dists/android/AndroidManifest.xml
backends/platform/psp/README.PSP
);