diff options
author | Alyssa Milburn | 2011-10-25 21:18:42 +0200 |
---|---|---|
committer | Alyssa Milburn | 2011-10-25 21:18:42 +0200 |
commit | 44b7f3aed52b1213995f9e19e4395f4350997b01 (patch) | |
tree | 88e3b4711c2da6524d9b5dbb423083c08ab1fb8f /backends/platform/android/org/scummvm | |
parent | 2967e2cd9172b2d2492e46c33968013b2bcbdbc1 (diff) | |
download | scummvm-rg350-44b7f3aed52b1213995f9e19e4395f4350997b01.tar.gz scummvm-rg350-44b7f3aed52b1213995f9e19e4395f4350997b01.tar.bz2 scummvm-rg350-44b7f3aed52b1213995f9e19e4395f4350997b01.zip |
ANDROID: Move from org.inodes.gus to org.scummvm.
Diffstat (limited to 'backends/platform/android/org/scummvm')
7 files changed, 1428 insertions, 0 deletions
diff --git a/backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java b/backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java new file mode 100644 index 0000000000..b593fc6abf --- /dev/null +++ b/backends/platform/android/org/scummvm/scummvm/EditableSurfaceView.java @@ -0,0 +1,61 @@ +package org.scummvm.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 false; + } + + 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); + } + + // Sends enter key + return super.performEditorAction(actionCode); + } + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + outAttrs.initialCapsMode = 0; + outAttrs.initialSelEnd = outAttrs.initialSelStart = -1; + outAttrs.inputType = (InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_VARIATION_NORMAL | + InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); + outAttrs.imeOptions = (EditorInfo.IME_ACTION_DONE | + EditorInfo.IME_FLAG_NO_EXTRACT_UI); + + return new MyInputConnection(); + } +} diff --git a/backends/platform/android/org/scummvm/scummvm/PluginProvider.java b/backends/platform/android/org/scummvm/scummvm/PluginProvider.java new file mode 100644 index 0000000000..0c43529f83 --- /dev/null +++ b/backends/platform/android/org/scummvm/scummvm/PluginProvider.java @@ -0,0 +1,53 @@ +package org.scummvm.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 { + private final static String LOG_TAG = "ScummVM"; + + public final static String META_UNPACK_LIB = + "org.scummvm.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(LOG_TAG, "Error finding my own info?", e); + return; + } + + String mylib = info.metaData.getString(META_UNPACK_LIB); + if (mylib != null) { + ArrayList<String> all_libs = + extras.getStringArrayList(ScummVMApplication.EXTRA_UNPACK_LIBS); + all_libs.add(new Uri.Builder() + .scheme("plugin") + .authority(context.getPackageName()) + .path(mylib) + .toString()); + + extras.putStringArrayList(ScummVMApplication.EXTRA_UNPACK_LIBS, + all_libs); + } + + setResultExtras(extras); + } +} diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java new file mode 100644 index 0000000000..3a25b54eeb --- /dev/null +++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java @@ -0,0 +1,451 @@ +package org.scummvm.scummvm; + +import android.util.Log; +import android.content.res.AssetManager; +import android.view.SurfaceHolder; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; + +import javax.microedition.khronos.opengles.GL10; +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.LinkedHashMap; + +public abstract class ScummVM implements SurfaceHolder.Callback, Runnable { + final protected static String LOG_TAG = "ScummVM"; + final private AssetManager _asset_manager; + final private Object _sem_surface; + + private EGL10 _egl; + private EGLDisplay _egl_display = EGL10.EGL_NO_DISPLAY; + private EGLConfig _egl_config; + private EGLContext _egl_context = EGL10.EGL_NO_CONTEXT; + private EGLSurface _egl_surface = EGL10.EGL_NO_SURFACE; + + private SurfaceHolder _surface_holder; + private AudioTrack _audio_track; + private int _sample_rate = 0; + private int _buffer_size = 0; + + private String[] _args; + + final private native void create(AssetManager asset_manager, + EGL10 egl, EGLDisplay egl_display, + AudioTrack audio_track, + int sample_rate, int buffer_size); + final private native void destroy(); + final private native void setSurface(int width, int height); + final private native int main(String[] args); + + // pause the engine and all native threads + final public native void setPause(boolean pause); + final public native void enableZoning(boolean enable); + // Feed an event to ScummVM. Safe to call from other threads. + final public native void pushEvent(int type, int arg1, int arg2, int arg3, + int arg4, int arg5); + + // Callbacks from C++ peer instance + abstract protected void getDPI(float[] values); + abstract protected void displayMessageOnOSD(String msg); + abstract protected void setWindowCaption(String caption); + abstract protected String[] getPluginDirectories(); + abstract protected void showVirtualKeyboard(boolean enable); + abstract protected String[] getSysArchives(); + + public ScummVM(AssetManager asset_manager, SurfaceHolder holder) { + _asset_manager = asset_manager; + _sem_surface = new Object(); + + holder.addCallback(this); + } + + // SurfaceHolder callback + final public void surfaceCreated(SurfaceHolder holder) { + Log.d(LOG_TAG, "surfaceCreated"); + + // no need to do anything, surfaceChanged() will be called in any case + } + + // SurfaceHolder callback + final public void surfaceChanged(SurfaceHolder holder, int format, + int width, int height) { + // the orientation may reset on standby mode and the theme manager + // could assert when using a portrait resolution. so lets not do that. + if (height > width) { + Log.d(LOG_TAG, String.format("Ignoring surfaceChanged: %dx%d (%d)", + width, height, format)); + return; + } + + Log.d(LOG_TAG, String.format("surfaceChanged: %dx%d (%d)", + width, height, format)); + + synchronized(_sem_surface) { + _surface_holder = holder; + _sem_surface.notifyAll(); + } + + // store values for the native code + setSurface(width, height); + } + + // SurfaceHolder callback + final public void surfaceDestroyed(SurfaceHolder holder) { + Log.d(LOG_TAG, "surfaceDestroyed"); + + synchronized(_sem_surface) { + _surface_holder = null; + _sem_surface.notifyAll(); + } + + // clear values for the native code + setSurface(0, 0); + } + + final public void setArgs(String[] args) { + _args = args; + } + + final public void run() { + try { + initAudio(); + initEGL(); + + // wait for the surfaceChanged callback + synchronized(_sem_surface) { + while (_surface_holder == null) + _sem_surface.wait(); + } + } catch (Exception e) { + deinitEGL(); + deinitAudio(); + + throw new RuntimeException("Error preparing the ScummVM thread", e); + } + + create(_asset_manager, _egl, _egl_display, + _audio_track, _sample_rate, _buffer_size); + + int res = main(_args); + + destroy(); + + deinitEGL(); + deinitAudio(); + + // On exit, tear everything down for a fresh restart next time. + System.exit(res); + } + + final private void initEGL() throws Exception { + _egl = (EGL10)EGLContext.getEGL(); + _egl_display = _egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + int[] version = new int[2]; + _egl.eglInitialize(_egl_display, version); + + int[] num_config = new int[1]; + _egl.eglGetConfigs(_egl_display, null, 0, num_config); + + final int numConfigs = num_config[0]; + + if (numConfigs <= 0) + throw new IllegalArgumentException("No EGL configs"); + + EGLConfig[] configs = new EGLConfig[numConfigs]; + _egl.eglGetConfigs(_egl_display, configs, numConfigs, num_config); + + // Android's eglChooseConfig is busted in several versions and + // devices so we have to filter/rank the configs ourselves. + _egl_config = chooseEglConfig(configs); + + _egl_context = _egl.eglCreateContext(_egl_display, _egl_config, + EGL10.EGL_NO_CONTEXT, null); + + if (_egl_context == EGL10.EGL_NO_CONTEXT) + throw new Exception(String.format("Failed to create context: 0x%x", + _egl.eglGetError())); + } + + // Callback from C++ peer instance + final protected EGLSurface initSurface() throws Exception { + _egl_surface = _egl.eglCreateWindowSurface(_egl_display, _egl_config, + _surface_holder, null); + + if (_egl_surface == EGL10.EGL_NO_SURFACE) + throw new Exception(String.format( + "eglCreateWindowSurface failed: 0x%x", _egl.eglGetError())); + + _egl.eglMakeCurrent(_egl_display, _egl_surface, _egl_surface, + _egl_context); + + GL10 gl = (GL10)_egl_context.getGL(); + + Log.i(LOG_TAG, String.format("Using EGL %s (%s); GL %s/%s (%s)", + _egl.eglQueryString(_egl_display, EGL10.EGL_VERSION), + _egl.eglQueryString(_egl_display, EGL10.EGL_VENDOR), + gl.glGetString(GL10.GL_VERSION), + gl.glGetString(GL10.GL_RENDERER), + gl.glGetString(GL10.GL_VENDOR))); + + return _egl_surface; + } + + // Callback from C++ peer instance + final protected void deinitSurface() { + if (_egl_display != EGL10.EGL_NO_DISPLAY) { + _egl.eglMakeCurrent(_egl_display, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + + if (_egl_surface != EGL10.EGL_NO_SURFACE) + _egl.eglDestroySurface(_egl_display, _egl_surface); + } + + _egl_surface = EGL10.EGL_NO_SURFACE; + } + + final private void deinitEGL() { + if (_egl_display != EGL10.EGL_NO_DISPLAY) { + _egl.eglMakeCurrent(_egl_display, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + + if (_egl_surface != EGL10.EGL_NO_SURFACE) + _egl.eglDestroySurface(_egl_display, _egl_surface); + + if (_egl_context != EGL10.EGL_NO_CONTEXT) + _egl.eglDestroyContext(_egl_display, _egl_context); + + _egl.eglTerminate(_egl_display); + } + + _egl_surface = EGL10.EGL_NO_SURFACE; + _egl_context = EGL10.EGL_NO_CONTEXT; + _egl_config = null; + _egl_display = EGL10.EGL_NO_DISPLAY; + _egl = null; + } + + final private void initAudio() throws Exception { + _sample_rate = AudioTrack.getNativeOutputSampleRate( + AudioManager.STREAM_MUSIC); + _buffer_size = AudioTrack.getMinBufferSize(_sample_rate, + AudioFormat.CHANNEL_CONFIGURATION_STEREO, + AudioFormat.ENCODING_PCM_16BIT); + + // ~50ms + int buffer_size_want = (_sample_rate * 2 * 2 / 20) & ~1023; + + if (_buffer_size < buffer_size_want) { + Log.w(LOG_TAG, String.format( + "adjusting audio buffer size (was: %d)", _buffer_size)); + + _buffer_size = buffer_size_want; + } + + Log.i(LOG_TAG, String.format("Using %d bytes buffer for %dHz audio", + _buffer_size, _sample_rate)); + + _audio_track = new AudioTrack(AudioManager.STREAM_MUSIC, + _sample_rate, + AudioFormat.CHANNEL_CONFIGURATION_STEREO, + AudioFormat.ENCODING_PCM_16BIT, + _buffer_size, + AudioTrack.MODE_STREAM); + + if (_audio_track.getState() != AudioTrack.STATE_INITIALIZED) + throw new Exception( + String.format("Error initializing AudioTrack: %d", + _audio_track.getState())); + } + + final private void deinitAudio() { + if (_audio_track != null) + _audio_track.stop(); + + _audio_track = null; + _buffer_size = 0; + _sample_rate = 0; + } + + private static final int[] s_eglAttribs = { + EGL10.EGL_CONFIG_ID, + EGL10.EGL_BUFFER_SIZE, + EGL10.EGL_RED_SIZE, + EGL10.EGL_GREEN_SIZE, + EGL10.EGL_BLUE_SIZE, + EGL10.EGL_ALPHA_SIZE, + EGL10.EGL_CONFIG_CAVEAT, + EGL10.EGL_DEPTH_SIZE, + EGL10.EGL_LEVEL, + EGL10.EGL_MAX_PBUFFER_WIDTH, + EGL10.EGL_MAX_PBUFFER_HEIGHT, + EGL10.EGL_MAX_PBUFFER_PIXELS, + EGL10.EGL_NATIVE_RENDERABLE, + EGL10.EGL_NATIVE_VISUAL_ID, + EGL10.EGL_NATIVE_VISUAL_TYPE, + EGL10.EGL_SAMPLE_BUFFERS, + EGL10.EGL_SAMPLES, + EGL10.EGL_STENCIL_SIZE, + EGL10.EGL_SURFACE_TYPE, + EGL10.EGL_TRANSPARENT_TYPE, + EGL10.EGL_TRANSPARENT_RED_VALUE, + EGL10.EGL_TRANSPARENT_GREEN_VALUE, + EGL10.EGL_TRANSPARENT_BLUE_VALUE + }; + + final private class EglAttribs extends LinkedHashMap<Integer, Integer> { + public EglAttribs(EGLConfig config) { + super(s_eglAttribs.length); + + int[] value = new int[1]; + + for (int i : s_eglAttribs) { + _egl.eglGetConfigAttrib(_egl_display, config, i, value); + + put(i, value[0]); + } + } + + private int weightBits(int attr, int size) { + final int value = get(attr); + + int score = 0; + + if (value == size || (size > 0 && value > size)) + score += 10; + + // penalize for wasted bits + score -= value - size; + + return score; + } + + public int weight() { + int score = 10000; + + if (get(EGL10.EGL_CONFIG_CAVEAT) != EGL10.EGL_NONE) + score -= 1000; + + // less MSAA is better + score -= get(EGL10.EGL_SAMPLES) * 100; + + // Must be at least 565, but then smaller is better + score += weightBits(EGL10.EGL_RED_SIZE, 5); + score += weightBits(EGL10.EGL_GREEN_SIZE, 6); + score += weightBits(EGL10.EGL_BLUE_SIZE, 5); + score += weightBits(EGL10.EGL_ALPHA_SIZE, 0); + score += weightBits(EGL10.EGL_DEPTH_SIZE, 0); + score += weightBits(EGL10.EGL_STENCIL_SIZE, 0); + + return score; + } + + public String toString() { + String s; + + if (get(EGL10.EGL_ALPHA_SIZE) > 0) + s = String.format("[%d] RGBA%d%d%d%d", + get(EGL10.EGL_CONFIG_ID), + get(EGL10.EGL_RED_SIZE), + get(EGL10.EGL_GREEN_SIZE), + get(EGL10.EGL_BLUE_SIZE), + get(EGL10.EGL_ALPHA_SIZE)); + else + s = String.format("[%d] RGB%d%d%d", + get(EGL10.EGL_CONFIG_ID), + get(EGL10.EGL_RED_SIZE), + get(EGL10.EGL_GREEN_SIZE), + get(EGL10.EGL_BLUE_SIZE)); + + if (get(EGL10.EGL_DEPTH_SIZE) > 0) + s += String.format(" D%d", get(EGL10.EGL_DEPTH_SIZE)); + + if (get(EGL10.EGL_STENCIL_SIZE) > 0) + s += String.format(" S%d", get(EGL10.EGL_STENCIL_SIZE)); + + if (get(EGL10.EGL_SAMPLES) > 0) + s += String.format(" MSAAx%d", get(EGL10.EGL_SAMPLES)); + + if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_WINDOW_BIT) > 0) + s += " W"; + if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_PBUFFER_BIT) > 0) + s += " P"; + if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_PIXMAP_BIT) > 0) + s += " X"; + + switch (get(EGL10.EGL_CONFIG_CAVEAT)) { + case EGL10.EGL_NONE: + break; + + case EGL10.EGL_SLOW_CONFIG: + s += " SLOW"; + break; + + case EGL10.EGL_NON_CONFORMANT_CONFIG: + s += " NON_CONFORMANT"; + + default: + s += String.format(" unknown CAVEAT 0x%x", + get(EGL10.EGL_CONFIG_CAVEAT)); + } + + return s; + } + }; + + final private EGLConfig chooseEglConfig(EGLConfig[] configs) { + EGLConfig res = configs[0]; + int bestScore = -1; + + Log.d(LOG_TAG, "EGL configs:"); + + for (EGLConfig config : configs) { + EglAttribs attr = new EglAttribs(config); + + // must have + if ((attr.get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_WINDOW_BIT) == 0) + continue; + + int score = attr.weight(); + + Log.d(LOG_TAG, String.format("%s (%d)", attr.toString(), score)); + + if (score > bestScore) { + res = config; + bestScore = score; + } + } + + if (bestScore < 0) + Log.e(LOG_TAG, + "Unable to find an acceptable EGL config, expect badness."); + + Log.d(LOG_TAG, String.format("Chosen EGL config: %s", + new EglAttribs(res).toString())); + + return res; + } + + static { + // For grabbing with gdb... + final boolean sleep_for_debugger = false; + if (sleep_for_debugger) { + try { + Thread.sleep(20 * 1000); + } catch (InterruptedException e) { + } + } + + File cache_dir = ScummVMApplication.getLastCacheDir(); + String libname = System.mapLibraryName("scummvm"); + File libpath = new File(cache_dir, libname); + + System.load(libpath.getPath()); + } +} diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java new file mode 100644 index 0000000000..a41e843323 --- /dev/null +++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java @@ -0,0 +1,224 @@ +package org.scummvm.scummvm; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Environment; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.SurfaceView; +import android.view.SurfaceHolder; +import android.view.MotionEvent; +import android.view.inputmethod.InputMethodManager; +import android.widget.Toast; + +public class ScummVMActivity extends Activity { + + private class MyScummVM extends ScummVM { + private boolean usingSmallScreen() { + // Multiple screen sizes came in with Android 1.6. Have + // to use reflection in order to continue supporting 1.5 + // devices :( + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(metrics); + + try { + // This 'density' term is very confusing. + int DENSITY_LOW = metrics.getClass().getField("DENSITY_LOW").getInt(null); + int densityDpi = metrics.getClass().getField("densityDpi").getInt(metrics); + return densityDpi <= DENSITY_LOW; + } catch (Exception e) { + return false; + } + } + + public MyScummVM(SurfaceHolder holder) { + super(ScummVMActivity.this.getAssets(), holder); + + // Enable ScummVM zoning on 'small' screens. + // FIXME make this optional for the user + // disabled for now since it crops too much + //enableZoning(usingSmallScreen()); + } + + @Override + protected void getDPI(float[] values) { + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(metrics); + + values[0] = metrics.xdpi; + values[1] = metrics.ydpi; + } + + @Override + protected void displayMessageOnOSD(String msg) { + Log.i(LOG_TAG, "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) { + runOnUiThread(new Runnable() { + public void run() { + showKeyboard(enable); + } + }); + } + + @Override + protected String[] getSysArchives() { + return new String[0]; + } + + } + + private MyScummVM _scummvm; + private ScummVMEvents _events; + private Thread _scummvm_thread; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + 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.requestFocus(); + + getFilesDir().mkdirs(); + + // Start ScummVM + _scummvm = new MyScummVM(main_surface.getHolder()); + + _scummvm.setArgs(new String[] { + "ScummVM", + "--config=" + getFileStreamPath("scummvmrc").getPath(), + "--path=" + Environment.getExternalStorageDirectory().getPath(), + "--gui-theme=scummmodern", + "--savepath=" + getDir("saves", 0).getPath() + }); + + _events = new ScummVMEvents(this, _scummvm); + + main_surface.setOnKeyListener(_events); + main_surface.setOnTouchListener(_events); + + _scummvm_thread = new Thread(_scummvm, "ScummVM"); + _scummvm_thread.start(); + } + + @Override + public void onStart() { + Log.d(ScummVM.LOG_TAG, "onStart"); + + super.onStart(); + } + + @Override + public void onResume() { + Log.d(ScummVM.LOG_TAG, "onResume"); + + super.onResume(); + + if (_scummvm != null) + _scummvm.setPause(false); + } + + @Override + public void onPause() { + Log.d(ScummVM.LOG_TAG, "onPause"); + + super.onPause(); + + if (_scummvm != null) + _scummvm.setPause(true); + } + + @Override + public void onStop() { + Log.d(ScummVM.LOG_TAG, "onStop"); + + super.onStop(); + } + + @Override + public void onDestroy() { + Log.d(ScummVM.LOG_TAG, "onDestroy"); + + super.onDestroy(); + + if (_events != null) { + _events.sendQuitEvent(); + + try { + // 1s timeout + _scummvm_thread.join(1000); + } catch (InterruptedException e) { + Log.i(ScummVM.LOG_TAG, "Error while joining ScummVM thread", e); + } + + _scummvm = null; + } + } + + @Override + public boolean onTrackballEvent(MotionEvent e) { + if (_events != null) + return _events.onTrackballEvent(e); + + return false; + } + + private void showKeyboard(boolean show) { + SurfaceView main_surface = (SurfaceView)findViewById(R.id.main_surface); + InputMethodManager imm = (InputMethodManager) + getSystemService(INPUT_METHOD_SERVICE); + + if (show) + imm.showSoftInput(main_surface, InputMethodManager.SHOW_IMPLICIT); + else + imm.hideSoftInputFromWindow(main_surface.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + } +} diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMApplication.java b/backends/platform/android/org/scummvm/scummvm/ScummVMApplication.java new file mode 100644 index 0000000000..9241cba918 --- /dev/null +++ b/backends/platform/android/org/scummvm/scummvm/ScummVMApplication.java @@ -0,0 +1,30 @@ +package org.scummvm.scummvm; + +import android.app.Application; + +import java.io.File; + +public class ScummVMApplication extends Application { + public final static String ACTION_PLUGIN_QUERY = "org.scummvm.scummvm.action.PLUGIN_QUERY"; + public final static String EXTRA_UNPACK_LIBS = "org.scummvm.scummvm.extra.UNPACK_LIBS"; + + private static File _cache_dir; + + @Override + public void onCreate() { + super.onCreate(); + + // This is still on /data :( + _cache_dir = getCacheDir(); + // This is mounted noexec :( + //cache_dir = new File(Environment.getExternalStorageDirectory(), + // "/.ScummVM.tmp"); + // This is owned by download manager and requires special + // permissions to access :( + //cache_dir = Environment.getDownloadCacheDirectory(); + } + + public static File getLastCacheDir() { + return _cache_dir; + } +} diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java b/backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java new file mode 100644 index 0000000000..86227b9352 --- /dev/null +++ b/backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java @@ -0,0 +1,231 @@ +package org.scummvm.scummvm; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.content.Context; +import android.view.KeyEvent; +import android.view.KeyCharacterMap; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.GestureDetector; +import android.view.inputmethod.InputMethodManager; + +public class ScummVMEvents implements + android.view.View.OnKeyListener, + android.view.View.OnTouchListener, + android.view.GestureDetector.OnGestureListener, + android.view.GestureDetector.OnDoubleTapListener { + + public static final int JE_SYS_KEY = 0; + public static final int JE_KEY = 1; + public static final int JE_DPAD = 2; + public static final int JE_DOWN = 3; + public static final int JE_SCROLL = 4; + public static final int JE_TAP = 5; + public static final int JE_DOUBLE_TAP = 6; + public static final int JE_MULTI = 7; + public static final int JE_BALL = 8; + public static final int JE_QUIT = 0x1000; + + final protected Context _context; + final protected ScummVM _scummvm; + final protected GestureDetector _gd; + final protected int _longPress; + + public ScummVMEvents(Context context, ScummVM scummvm) { + _context = context; + _scummvm = scummvm; + + _gd = new GestureDetector(context, this); + _gd.setOnDoubleTapListener(this); + _gd.setIsLongpressEnabled(false); + + _longPress = ViewConfiguration.getLongPressTimeout(); + } + + final public void sendQuitEvent() { + _scummvm.pushEvent(JE_QUIT, 0, 0, 0, 0, 0); + } + + public boolean onTrackballEvent(MotionEvent e) { + _scummvm.pushEvent(JE_BALL, e.getAction(), + (int)(e.getX() * e.getXPrecision() * 100), + (int)(e.getY() * e.getYPrecision() * 100), + 0, 0); + return true; + } + + final static int MSG_MENU_LONG_PRESS = 1; + + final private Handler keyHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_MENU_LONG_PRESS) { + InputMethodManager imm = (InputMethodManager) + _context.getSystemService(_context.INPUT_METHOD_SERVICE); + + if (imm != null) + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + }; + + // OnKeyListener + final public boolean onKey(View v, int keyCode, KeyEvent e) { + final int action = e.getAction(); + + if (e.isSystem()) { + // filter what we handle + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_CAMERA: + case KeyEvent.KEYCODE_SEARCH: + break; + + default: + return false; + } + + // no repeats for system keys + if (e.getRepeatCount() > 0) + return false; + + // 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 (keyCode == KeyEvent.KEYCODE_MENU) { + final boolean fired = + !keyHandler.hasMessages(MSG_MENU_LONG_PRESS); + + keyHandler.removeMessages(MSG_MENU_LONG_PRESS); + + if (action == KeyEvent.ACTION_DOWN) { + keyHandler.sendMessageDelayed(keyHandler.obtainMessage( + MSG_MENU_LONG_PRESS), _longPress); + return true; + } + + if (fired) + return true; + + // only send up events of the menu button to the native side + if (action != KeyEvent.ACTION_UP) + return true; + } + + _scummvm.pushEvent(JE_SYS_KEY, action, keyCode, 0, 0, 0); + + return true; + } + + // sequence of characters + if (action == KeyEvent.ACTION_MULTIPLE && + keyCode == KeyEvent.KEYCODE_UNKNOWN) { + final KeyCharacterMap m = KeyCharacterMap.load(e.getDeviceId()); + final KeyEvent[] es = m.getEvents(e.getCharacters().toCharArray()); + + if (es == null) + return true; + + for (KeyEvent s : es) { + _scummvm.pushEvent(JE_KEY, s.getAction(), s.getKeyCode(), + s.getUnicodeChar() & KeyCharacterMap.COMBINING_ACCENT_MASK, + s.getMetaState(), s.getRepeatCount()); + } + + return true; + } + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + _scummvm.pushEvent(JE_DPAD, action, keyCode, + (int)(e.getEventTime() - e.getDownTime()), + e.getRepeatCount(), 0); + return true; + } + + _scummvm.pushEvent(JE_KEY, action, keyCode, + e.getUnicodeChar() & KeyCharacterMap.COMBINING_ACCENT_MASK, + e.getMetaState(), e.getRepeatCount()); + + return true; + } + + // OnTouchListener + final public boolean onTouch(View v, MotionEvent e) { + final int action = e.getAction(); + + // constants from APIv5: + // (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT + final int pointer = (action & 0xff00) >> 8; + + if (pointer > 0) { + _scummvm.pushEvent(JE_MULTI, pointer, action & 0xff, // ACTION_MASK + (int)e.getX(), (int)e.getY(), 0); + return true; + } + + return _gd.onTouchEvent(e); + } + + // OnGestureListener + final public boolean onDown(MotionEvent e) { + _scummvm.pushEvent(JE_DOWN, (int)e.getX(), (int)e.getY(), 0, 0, 0); + return true; + } + + final public boolean onFling(MotionEvent e1, MotionEvent e2, + float velocityX, float velocityY) { + //Log.d(ScummVM.LOG_TAG, String.format("onFling: %s -> %s (%.3f %.3f)", + // e1.toString(), e2.toString(), + // velocityX, velocityY)); + + return true; + } + + final public void onLongPress(MotionEvent e) { + // disabled, interferes with drag&drop + } + + final public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + _scummvm.pushEvent(JE_SCROLL, (int)e1.getX(), (int)e1.getY(), + (int)e2.getX(), (int)e2.getY(), 0); + + return true; + } + + final public void onShowPress(MotionEvent e) { + } + + final public boolean onSingleTapUp(MotionEvent e) { + _scummvm.pushEvent(JE_TAP, (int)e.getX(), (int)e.getY(), + (int)(e.getEventTime() - e.getDownTime()), 0, 0); + + return true; + } + + // OnDoubleTapListener + final public boolean onDoubleTap(MotionEvent e) { + return true; + } + + final public boolean onDoubleTapEvent(MotionEvent e) { + _scummvm.pushEvent(JE_DOUBLE_TAP, (int)e.getX(), (int)e.getY(), + e.getAction(), 0, 0); + + return true; + } + + final public boolean onSingleTapConfirmed(MotionEvent e) { + return true; + } +} diff --git a/backends/platform/android/org/scummvm/scummvm/Unpacker.java b/backends/platform/android/org/scummvm/scummvm/Unpacker.java new file mode 100644 index 0000000000..4564d96622 --- /dev/null +++ b/backends/platform/android/org/scummvm/scummvm/Unpacker.java @@ -0,0 +1,378 @@ +package org.scummvm.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 { + protected final static String LOG_TAG = "ScummVM"; + // TODO don't hardcode this + private final static boolean PLUGINS_ENABLED = false; + private final static String META_NEXT_ACTIVITY = + "org.scummvm.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; + + // Android 3.1+ only + public static final int FLAG_INCLUDE_STOPPED_PACKAGES = 32; + + 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.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(LOG_TAG, + "Starting next activity with intent " + intent); + startActivity(intent); + } else { + Log.w(LOG_TAG, + "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(LOG_TAG, "Package " + pkg + + " not found", e); + continue; + } catch (IOException e) { + // FIXME: show some sort of GUI error dialog + Log.e(LOG_TAG, + "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(LOG_TAG, + "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(LOG_TAG, + "Replacing " + dest.getPath() + + " old.mtime=" + dest.lastModified() + + " new.mtime=" + zipentry.getTime() + + " old.size=" + dest.length() + + " new.size=" + zipentry.getSize()); + else + Log.i(LOG_TAG, + "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(LOG_TAG, "Error unpacking plugin", e); + } + } + + if (progress != total_size) + Log.d(LOG_TAG, "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(LOG_TAG, + "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); + // Android 3.1 defaults to FLAG_EXCLUDE_STOPPED_PACKAGES, and since + // none of our plugins will ever be running, that is not helpful + intent.setFlags(FLAG_INCLUDE_STOPPED_PACKAGES); + 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_ENABLED && 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(LOG_TAG, + "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(LOG_TAG, "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(LOG_TAG, "Unable to find my own meta-data", e); + return new Bundle(); + } + } +} |