/* 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 "backends/base-backend.h" #include "base/main.h" #include "graphics/surface.h" #include "backends/platform/android/android.h" #include "backends/platform/android/video.h" #include #include #include #include #include #include #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 "audio/mixer_intern.h" #include "backends/platform/android/asset-archive.h" const char *android_log_tag = "ScummVM"; // This replaces the bionic libc assert functions with something that // actually prints the assertion failure before aborting. extern "C" { void __assert(const char *file, int line, const char *expr) { __android_log_assert(expr, android_log_tag, "Assertion failure: '%s' in %s:%d", expr, file, line); } void __assert2(const char *file, int line, const char *func, const char *expr) { __android_log_assert(expr, android_log_tag, "Assertion failure: '%s' in %s:%d (%s)", expr, file, line, func); } } #ifdef ANDROID_DEBUG_GL static const char *getGlErrStr(GLenum error) { switch (error) { case GL_INVALID_ENUM: return "GL_INVALID_ENUM"; case GL_INVALID_VALUE: return "GL_INVALID_VALUE"; 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; } void checkGlError(const char *expr, const char *file, int line) { GLenum error = glGetError(); if (error != GL_NO_ERROR) LOGE("GL ERROR: %s on %s (%s:%d)", getGlErrStr(error), expr, file, line); } #endif 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 = 0; jint res = cached_jvm->GetEnv((void **)&env, JNI_VERSION_1_2); if (res != JNI_OK) { LOGE("GetEnv() failed: %d", res); abort(); } return env; } static void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg) { jclass cls = env->FindClass(name); // if cls is 0, an exception has already been thrown if (cls != 0) env->ThrowNew(cls, msg); env->DeleteLocalRef(cls); } // floating point. use sparingly template static inline T scalef(T in, float numerator, float denominator) { return static_cast(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 class OSystem_Android : public BaseBackend, public PaletteManager { private: // back pointer to (java) peer instance jobject _back_ptr; 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; jmethodID MID_swapBuffers; int _screen_changeid; int _egl_surface_width; int _egl_surface_height; bool _force_redraw; // Game layer GLESPaletteTexture *_game_texture; int _shake_offset; Common::Rect _focus_rect; // 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 _event_queue; MutexRef _event_queue_lock; bool _timer_thread_exit; pthread_t _timer_thread; static void *timerThreadFunc(void *arg); bool _enable_zoning; 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; void enableZoning(bool enable) { _enable_zoning = enable; } void setSurfaceSize(int width, int height) { _egl_surface_width = width; _egl_surface_height = height; } 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 PaletteManager *getPaletteManager() { return this; } protected: // PaletteManager API virtual void setPalette(const byte *colors, uint start, uint num); virtual void grabPalette(byte *colors, uint start, uint num); public: 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(); // RGBA 4444 virtual Graphics::PixelFormat getOverlayFormat() const { 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 logMessage(LogMessageType::Type type, const char *message); virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0); }; OSystem_Android::OSystem_Android(jobject am) : _back_ptr(0), _screen_changeid(0), _force_redraw(false), _game_texture(0), _overlay_texture(0), _mouse_texture(0), _use_mouse_palette(false), _show_mouse(false), _show_overlay(false), _enable_zoning(false), _savefile(0), _mixer(0), _timer(0), _fsFactory(new POSIXFilesystemFactory()), _asset_archive(new AndroidAssetArchive(am)), _shake_offset(0), _event_queue_lock(createMutex()) { } OSystem_Android::~OSystem_Android() { ENTER(); 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 == 0) \ 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"); FIND_METHOD(swapBuffers, "()Z"); #undef FIND_METHOD return true; } static void ScummVM_create(JNIEnv *env, jobject self, jobject am) { OSystem_Android *cpp_obj = new OSystem_Android(am); // Exception already thrown by initJavaHooks? if (!cpp_obj->initJavaHooks(env, self)) 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, 0); if (buf == 0) { warning("Unable to get Java audio byte array. Skipping"); return; } Audio::MixerImpl *mixer = static_cast(cpp_obj->getMixer()); assert(mixer); mixer->mixCallback(reinterpret_cast(buf), len); env->ReleaseByteArrayElements(jbuf, buf, 0); } static void ScummVM_setConfManInt(JNIEnv *env, jclass cls, jstring key_obj, jint value) { ENTER("%p, %d", key_obj, (int)value); const char *key = env->GetStringUTFChars(key_obj, 0); if (key == 0) 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("%p, %p", key_obj, value_obj); const char *key = env->GetStringUTFChars(key_obj, 0); if (key == 0) return; const char *value = env->GetStringUTFChars(value_obj, 0); if (value == 0) { 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); JNIEnv *env = 0; jint res = cached_jvm->AttachCurrentThread(&env, 0); if (res != JNI_OK) { LOGE("AttachCurrentThread() failed: %d", res); abort(); } struct timespec tv; tv.tv_sec = 0; tv.tv_nsec = 100 * 1000 * 1000; // 100ms while (!system->_timer_thread_exit) { timer->handler(); nanosleep(&tv, 0); } res = cached_jvm->DetachCurrentThread(); if (res != JNI_OK) { LOGE("DetachCurrentThread() failed: %d", res); abort(); } return 0; } void OSystem_Android::initBackend() { ENTER(); 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, 0); 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, 0, timerThreadFunc, this); OSystem::initBackend(); setupScummVMSurface(); } void OSystem_Android::addPluginDirectories(Common::FSList &dirs) const { ENTER(); 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 == 0) continue; const char *path = env->GetStringUTFChars(path_obj, 0); if (path == 0) { 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("%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("%s", mode); return true; } bool OSystem_Android::setGraphicsMode(int mode) { ENTER("%d", mode); return true; } int OSystem_Android::getGraphicsMode() const { return 1; } void OSystem_Android::setupScummVMSurface() { ENTER(); JNIEnv *env = JNU_GetEnv(); env->CallVoidMethod(_back_ptr, MID_setupScummVMSurface); if (env->ExceptionCheck()) return; // EGL set up with a new surface. Initialise OpenGLES context. GLESTexture::initGLExtensions(); // Turn off anything that looks like 3D ;) GLCALL(glDisable(GL_CULL_FACE)); GLCALL(glDisable(GL_DEPTH_TEST)); GLCALL(glDisable(GL_LIGHTING)); GLCALL(glDisable(GL_FOG)); GLCALL(glDisable(GL_DITHER)); GLCALL(glShadeModel(GL_FLAT)); GLCALL(glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST)); GLCALL(glEnable(GL_BLEND)); GLCALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); GLCALL(glEnableClientState(GL_VERTEX_ARRAY)); GLCALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY)); GLCALL(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(); GLCALL(glViewport(0, 0, _egl_surface_width, _egl_surface_height)); GLCALL(glMatrixMode(GL_PROJECTION)); GLCALL(glLoadIdentity()); GLCALL(glOrthof(0, _egl_surface_width, _egl_surface_height, 0, -1, 1)); GLCALL(glMatrixMode(GL_MODELVIEW)); GLCALL(glLoadIdentity()); clearFocusRectangle(); } void OSystem_Android::destroyScummVMSurface() { 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("%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("%p, %u, %u", colors, start, num); if (!_use_mouse_palette) _setCursorPalette(colors, start, num); memcpy(_game_texture->palette() + start * 3, colors, num * 3); } void OSystem_Android::grabPalette(byte *colors, uint start, uint num) { ENTER("%p, %u, %u", colors, start, num); memcpy(colors, _game_texture->palette_const() + start * 3, num * 3); } void OSystem_Android::copyRectToScreen(const byte *buf, int pitch, int x, int y, int w, int h) { ENTER("%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(); if (!_force_redraw && !_game_texture->dirty() && !_overlay_texture->dirty() && !_mouse_texture->dirty()) return; _force_redraw = false; GLCALL(glPushMatrix()); if (_shake_offset != 0 || (!_focus_rect.isEmpty() && !Common::Rect(_game_texture->width(), _game_texture->height()).contains(_focus_rect))) { // These are the only cases where _game_texture doesn't // cover the entire screen. GLCALL(glClearColorx(0, 0, 0, 1 << 16)); GLCALL(glClear(GL_COLOR_BUFFER_BIT)); // Move everything up by _shake_offset (game) pixels GLCALL(glTranslatex(0, -_shake_offset << 16, 0)); } if (_focus_rect.isEmpty()) { _game_texture->drawTexture(0, 0, _egl_surface_width, _egl_surface_height); } else { GLCALL(glPushMatrix()); GLCALL(glScalex(xdiv(_egl_surface_width, _focus_rect.width()), xdiv(_egl_surface_height, _focus_rect.height()), 1 << 16)); GLCALL(glTranslatex(-_focus_rect.left << 16, -_focus_rect.top << 16, 0)); GLCALL(glScalex(xdiv(_game_texture->width(), _egl_surface_width), xdiv(_game_texture->height(), _egl_surface_height), 1 << 16)); _game_texture->drawTexture(0, 0, _egl_surface_width, _egl_surface_height); GLCALL(glPopMatrix()); } int cs = _mouse_targetscale; if (_show_overlay) { // ugly, but the modern theme sets a wacko factor, only god knows why cs = 1; GLCALL(_overlay_texture->drawTexture(0, 0, _egl_surface_width, _egl_surface_height)); } if (_show_mouse) { GLCALL(glPushMatrix()); // Scale up ScummVM -> OpenGL (pixel) coordinates int texwidth, texheight; if (_show_overlay) { texwidth = getOverlayWidth(); texheight = getOverlayHeight(); } else { texwidth = getWidth(); texheight = getHeight(); } GLCALL(glScalex(xdiv(_egl_surface_width, texwidth), xdiv(_egl_surface_height, texheight), 1 << 16)); GLCALL(glTranslatex((-_mouse_hotspot.x * cs) << 16, (-_mouse_hotspot.y * cs) << 16, 0)); // Note the extra half texel to position the mouse in // the middle of the x,y square: const Common::Point& mouse = getEventManager()->getMousePos(); GLCALL(glTranslatex((mouse.x << 16) | 1 << 15, (mouse.y << 16) | 1 << 15, 0)); GLCALL(glScalex(cs << 16, cs << 16, 1 << 16)); _mouse_texture->drawTexture(); GLCALL(glPopMatrix()); } GLCALL(glPopMatrix()); JNIEnv *env = JNU_GetEnv(); if (!env->CallBooleanMethod(_back_ptr, MID_swapBuffers)) { // Context lost -> need to reinit GL destroyScummVMSurface(); setupScummVMSurface(); } } Graphics::Surface *OSystem_Android::lockScreen() { ENTER(); Graphics::Surface *surface = _game_texture->surface(); assert(surface->pixels); return surface; } void OSystem_Android::unlockScreen() { ENTER(); assert(_game_texture->dirty()); } void OSystem_Android::setShakePos(int shake_offset) { ENTER("%d", shake_offset); if (_shake_offset != shake_offset) { _shake_offset = shake_offset; _force_redraw = true; } } void OSystem_Android::fillScreen(uint32 col) { ENTER("%u", col); assert(col < 256); _game_texture->fillBuffer(col); } void OSystem_Android::setFocusRectangle(const Common::Rect& rect) { ENTER("%d, %d, %d, %d", rect.left, rect.top, rect.right, rect.bottom); if (_enable_zoning) { _focus_rect = rect; _force_redraw = true; } } void OSystem_Android::clearFocusRectangle() { ENTER(); if (_enable_zoning) { _focus_rect = Common::Rect(); _force_redraw = true; } } void OSystem_Android::showOverlay() { ENTER(); _show_overlay = true; _force_redraw = true; } void OSystem_Android::hideOverlay() { ENTER(); _show_overlay = false; _force_redraw = true; } void OSystem_Android::clearOverlay() { ENTER(); _overlay_texture->fillBuffer(0); // Shouldn't need this, but works around a 'blank screen' bug on Nexus1 updateScreen(); } void OSystem_Android::grabOverlay(OverlayColor *buf, int pitch) { ENTER("%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])); // This 'pitch' is pixels not bytes buf += pitch; } while (--h); } void OSystem_Android::copyRectToOverlay(const OverlayColor *buf, int pitch, int x, int y, int w, int h) { ENTER("%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])); // Shouldn't need this, but works around a 'blank screen' bug on Nexus1? updateScreen(); } int16 OSystem_Android::getOverlayHeight() { return _overlay_texture->height(); } int16 OSystem_Android::getOverlayWidth() { return _overlay_texture->width(); } bool OSystem_Android::showMouse(bool visible) { ENTER("%d", visible); _show_mouse = visible; return true; } void OSystem_Android::warpMouse(int x, int y) { ENTER("%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("%p, %u, %u, %d, %d, %u, %d, %p", buf, w, h, hotspotX, hotspotY, 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 += 3; } while (--num); } void OSystem_Android::setCursorPalette(const byte *colors, uint start, uint num) { ENTER("%p, %u, %u", colors, start, num); _setCursorPalette(colors, start, num); _use_mouse_palette = true; } void OSystem_Android::disableCursorPalette(bool disable) { ENTER("%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(); 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: { // relative mouse hack if (event.kbd.flags == 1) { // 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(_overlay_texture) : static_cast(_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, clear relative flag tail.kbd.flags = 0; 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(env->GetIntField( java_event, FID_Event_kbd_ascii)); event.kbd.flags = static_cast(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, 0); 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 0; } 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(); _timer_thread_exit = true; pthread_join(_timer_thread, 0); } void OSystem_Android::setWindowCaption(const char *caption) { ENTER("%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("%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("%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(0); 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, 0); if (path != 0) { s.addDirectory(path, path, priority); env->ReleaseStringUTFChars(path_obj, path); } env->DeleteLocalRef(path_obj); } } void OSystem_Android::logMessage(LogMessageType::Type type, const char *message) { switch (type) { case LogMessageType::kDebug: __android_log_write(ANDROID_LOG_DEBUG, android_log_tag, message); break; case LogMessageType::kWarning: __android_log_write(ANDROID_LOG_WARN, android_log_tag, message); break; case LogMessageType::kError: __android_log_write(ANDROID_LOG_ERROR, android_log_tag, message); break; } } 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]; // note use in cleanup loop below int nargs; for (nargs = 0; nargs < argc; ++nargs) { jstring arg = (jstring)env->GetObjectArrayElement(args, nargs); if (arg == 0) { argv[nargs] = 0; } else { const char *cstr = env->GetStringUTFChars(arg, 0); argv[nargs] = const_cast(cstr); // exception already thrown? if (cstr == 0) goto cleanup; } env->DeleteLocalRef(arg); } g_system = cpp_obj; assert(g_system); LOGI("Entering scummvm_main with %d args", argc); res = scummvm_main(argc, argv); LOGI("Exiting scummvm_main"); g_system->quit(); cleanup: nargs--; for (int i = 0; i < nargs; ++i) { if (argv[i] == 0) continue; jstring arg = (jstring)env->GetObjectArrayElement(args, nargs); // Exception already thrown? if (arg == 0) 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 static void ScummVM_enableZoning(JNIEnv *env, jobject self, jboolean enable) { OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self); cpp_obj->enableZoning(enable); } static void ScummVM_setSurfaceSize(JNIEnv *env, jobject self, jint width, jint height) { OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self); cpp_obj->setSurfaceSize(width, height); } 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 }, { "enableZoning", "(Z)V", (void *)ScummVM_enableZoning }, { "setSurfaceSize", "(II)V", (void *)ScummVM_setSurfaceSize }, }; 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 == 0) 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 == 0) return JNI_ERR; jclass event = env->FindClass("org/inodes/gus/scummvm/Event"); if (event == 0) return JNI_ERR; FID_Event_type = env->GetFieldID(event, "type", "I"); if (FID_Event_type == 0) return JNI_ERR; FID_Event_synthetic = env->GetFieldID(event, "synthetic", "Z"); if (FID_Event_synthetic == 0) return JNI_ERR; FID_Event_kbd_keycode = env->GetFieldID(event, "kbd_keycode", "I"); if (FID_Event_kbd_keycode == 0) return JNI_ERR; FID_Event_kbd_ascii = env->GetFieldID(event, "kbd_ascii", "I"); if (FID_Event_kbd_ascii == 0) return JNI_ERR; FID_Event_kbd_flags = env->GetFieldID(event, "kbd_flags", "I"); if (FID_Event_kbd_flags == 0) return JNI_ERR; FID_Event_mouse_x = env->GetFieldID(event, "mouse_x", "I"); if (FID_Event_mouse_x == 0) return JNI_ERR; FID_Event_mouse_y = env->GetFieldID(event, "mouse_y", "I"); if (FID_Event_mouse_y == 0) return JNI_ERR; FID_Event_mouse_relative = env->GetFieldID(event, "mouse_relative", "Z"); if (FID_Event_mouse_relative == 0) return JNI_ERR; cls = env->FindClass("java/lang/Object"); if (cls == 0) return JNI_ERR; MID_Object_wait = env->GetMethodID(cls, "wait", "()V"); if (MID_Object_wait == 0) return JNI_ERR; return JNI_VERSION_1_2; } #endif