diff --git a/android/.classpath b/android/.classpath
new file mode 100644
index 0000000000..a4763d1eec
--- /dev/null
+++ b/android/.classpath
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000000..021b4205f9
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,2 @@
+gen
+bin
diff --git a/android/.project b/android/.project
new file mode 100644
index 0000000000..790a38db49
--- /dev/null
+++ b/android/.project
@@ -0,0 +1,33 @@
+
+
+ libnative
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
new file mode 100644
index 0000000000..d2dd97c058
--- /dev/null
+++ b/android/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/proguard-project.txt b/android/proguard-project.txt
new file mode 100644
index 0000000000..f2fe1559a2
--- /dev/null
+++ b/android/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/android/project.properties b/android/project.properties
new file mode 100644
index 0000000000..8061f9c3fe
--- /dev/null
+++ b/android/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}\tools\proguard\proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-8
+android.library=true
diff --git a/android/res/drawable-hdpi/ic_launcher.png b/android/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..96a442e5b8
Binary files /dev/null and b/android/res/drawable-hdpi/ic_launcher.png differ
diff --git a/android/res/drawable-ldpi/ic_launcher.png b/android/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000000..99238729d8
Binary files /dev/null and b/android/res/drawable-ldpi/ic_launcher.png differ
diff --git a/android/res/drawable-mdpi/ic_launcher.png b/android/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..359047dfa4
Binary files /dev/null and b/android/res/drawable-mdpi/ic_launcher.png differ
diff --git a/android/res/drawable-xhdpi/ic_launcher.png b/android/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..71c6d760f0
Binary files /dev/null and b/android/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/android/res/layout/main.xml b/android/res/layout/main.xml
new file mode 100644
index 0000000000..bc12cd8231
--- /dev/null
+++ b/android/res/layout/main.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml
new file mode 100644
index 0000000000..6a272c9cf2
--- /dev/null
+++ b/android/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ Hello World!
+ libnative
+
+
\ No newline at end of file
diff --git a/android/src/com/turboviking/libnative/NativeActivity.java b/android/src/com/turboviking/libnative/NativeActivity.java
new file mode 100644
index 0000000000..701a73e2d2
--- /dev/null
+++ b/android/src/com/turboviking/libnative/NativeActivity.java
@@ -0,0 +1,449 @@
+package com.turboviking.libnative;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.UUID;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ConfigurationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.net.Uri;
+import android.opengl.GLSurfaceView;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+class NativeRenderer implements GLSurfaceView.Renderer {
+ private static String TAG = "RollerballRenderer";
+ NativeActivity mActivity;
+
+ NativeRenderer(NativeActivity act) {
+ mActivity = act;
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 unused, EGLConfig config) {
+ Log.i(TAG, "onSurfaceCreated");
+ displayInit();
+ }
+
+ @Override
+ public void onDrawFrame(GL10 unused /*use GLES20*/) {
+ displayRender();
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 unused, int width, int height) {
+ Log.i(TAG, "onSurfaceChanged");
+ displayResize(width, height);
+ }
+
+
+ // NATIVE METHODS
+
+ public native void displayInit();
+ // Note: This also means "device lost" and you should reload
+ // all buffered objects.
+ public native void displayResize(int w, int h);
+ public native void displayRender();
+
+ // called by the C++ code through JNI. Dispatch anything we can't directly handle
+ // on the gfx thread to the UI thread.
+ public void postCommand(String command, String parameter) {
+ final String cmd = command;
+ final String param = parameter;
+ mActivity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ NativeRenderer.this.mActivity.processCommand(cmd, param);
+ }
+ });
+ }
+}
+
+// Touch- and sensor-enabled GLSurfaceView.
+class NativeGLView extends GLSurfaceView implements SensorEventListener {
+ private static String TAG = "NativeGLView";
+ private SensorManager mSensorManager;
+ private Sensor mAccelerometer;
+
+ public NativeGLView(NativeActivity activity) {
+ super(activity);
+ setEGLContextClientVersion(2);
+ // setEGLConfigChooser(5, 5, 5, 0, 16, 0);
+ // setDebugFlags(DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS);
+ mSensorManager = (SensorManager)activity.getSystemService(Activity.SENSOR_SERVICE);
+ mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+
+ // This needs fleshing out. A lot.
+ // Going to want multitouch eventually.
+ public boolean onTouchEvent(final MotionEvent event) {
+ int code = 0;
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ code = 1;
+ } else if (event.getAction() == MotionEvent.ACTION_UP) {
+ code = 2;
+ } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ code = 3;
+ } else {
+ return true;
+ }
+ NativeApp.touch((int)event.getRawX(), (int)event.getRawY(), code);
+ return true;
+ }
+
+ // Sensor management
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int arg1) {
+ Log.i(TAG, "onAccuracyChanged");
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) {
+ return;
+ }
+ // Can also look at event.timestamp for accuracy magic
+ NativeApp.accelerometer(event.values[0], event.values[1], event.values[2]);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mSensorManager.unregisterListener(this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
+ }
+}
+
+
+class NativeAudioPlayer {
+ private String TAG = "NativeAudioPlayer";
+ private Thread thread;
+ private boolean playing_;
+
+ // Calling stop() is allowed at any time, whether stopped or not.
+ // If playing, blocks until not.
+ public synchronized void stop() {
+ if (thread != null) {
+ waitUntilDone();
+ } else {
+ Log.e(TAG, "Was already stopped");
+ }
+ }
+
+ // If not playing, make sure we're playing.
+ public synchronized void play() {
+ if (thread == null) {
+ playStreaming();
+ } else {
+ Log.e(TAG, "Was already playing");
+ }
+ }
+
+ private void playStreaming() {
+ playing_ = true;
+ thread = new Thread(new Runnable() {
+ public void run() {
+ playThread();
+ }
+ });
+ thread.start();
+ }
+
+ private void waitUntilDone() {
+ if (!playing_) {
+ Log.i(TAG, "not playing.");
+ }
+ try {
+ playing_ = false;
+ Log.e(TAG, "waitUntilDone: Joining audio thread.");
+ thread.join();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "INterrupted!");
+ e.printStackTrace();
+ }
+ thread = null;
+ Log.i(TAG, "Finished waitUntilDone");
+ }
+
+ private void playThread() {
+ try {
+ Log.i(TAG, "Thread started.");
+ // Get the smallest possible buffer size (which is annoyingly huge).
+ int buffer_size_bytes = AudioTrack.getMinBufferSize(
+ 44100,
+ AudioFormat.CHANNEL_CONFIGURATION_STEREO,
+ AudioFormat.ENCODING_PCM_16BIT);
+
+ // Round buffer_size_bytes up to an even multiple of 128 for convenience.
+ buffer_size_bytes = (buffer_size_bytes + 127) & ~127;
+
+ AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
+ 44100,
+ AudioFormat.CHANNEL_CONFIGURATION_STEREO,
+ AudioFormat.ENCODING_PCM_16BIT,
+ buffer_size_bytes,
+ AudioTrack.MODE_STREAM);
+
+ int buffer_size = buffer_size_bytes / 2;
+ short [] buffer = new short[buffer_size];
+ audioTrack.play();
+ Log.i(TAG, "Playing... minBuffersize = " + buffer_size);
+ while (playing_) {
+ NativeApp.audioRender(buffer);
+ audioTrack.write(buffer, 0, buffer_size);
+ }
+ audioTrack.stop();
+ audioTrack.release();
+ Log.i(TAG, "Stopped playing.");
+ } catch (Throwable t) {
+ Log.e(TAG, "Playback Failed");
+ t.printStackTrace();
+ Log.e(TAG, t.toString());
+ }
+ }
+}
+
+class Installation {
+ private static String sID = null;
+ private static final String INSTALLATION = "INSTALLATION";
+
+ public synchronized static String id(Context context) {
+ if (sID == null) {
+ File installation = new File(context.getFilesDir(), INSTALLATION);
+ try {
+ if (!installation.exists())
+ writeInstallationFile(installation);
+ sID = readInstallationFile(installation);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return sID;
+ }
+
+ private static String readInstallationFile(File installation) throws IOException {
+ RandomAccessFile f = new RandomAccessFile(installation, "r");
+ byte[] bytes = new byte[(int) f.length()];
+ f.readFully(bytes);
+ f.close();
+ return new String(bytes);
+ }
+
+ private static void writeInstallationFile(File installation) throws IOException {
+ FileOutputStream out = new FileOutputStream(installation);
+ String id = UUID.randomUUID().toString();
+ out.write(id.getBytes());
+ out.close();
+ }
+}
+
+
+public class NativeActivity extends Activity {
+ // Remember to loadLibrary your JNI .so in a static {} block
+
+ // Adjust these as necessary
+ private static String TAG = "NativeActivity";
+ String packageName = "com.turboviking.rollerball";
+
+ // Graphics and audio interfaces
+ private GLSurfaceView mGLSurfaceView;
+ private NativeAudioPlayer audioPlayer;
+
+
+ public static String runCommand;
+ public static String commandParameter;
+
+ public static String installID;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ super.onCreate(savedInstanceState);
+ Log.i(TAG, "onCreate");
+ installID = Installation.id(this);
+ // Get system information
+ ApplicationInfo appInfo = null;
+ PackageManager packMgmr = getPackageManager();
+ try {
+ appInfo = packMgmr.getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Unable to locate assets, aborting...");
+ }
+ File sdcard = Environment.getExternalStorageDirectory();
+ Display display = ((WindowManager)this.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ int scrPixelFormat = display.getPixelFormat();
+ int scrWidth = display.getWidth();
+ int scrHeight = display.getHeight();
+ float scrRefreshRate = (float)display.getRefreshRate();
+ String externalStorageDir = sdcard.getAbsolutePath();
+ String dataDir = this.getFilesDir().getAbsolutePath();
+ String apkFilePath = appInfo.sourceDir;
+ NativeApp.init(scrWidth, scrHeight, apkFilePath, dataDir, externalStorageDir, installID);
+
+ // Keep the screen bright - very annoying if it goes dark when tilting away
+ Window window = this.getWindow();
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ Log.i(TAG, "W : " + scrWidth + " H: " + scrHeight + " rate: " + scrRefreshRate + " fmt: " + scrPixelFormat);
+
+ // Initialize Graphics
+ if (!detectOpenGLES20()) {
+ throw new RuntimeException("Application requires OpenGL ES 2.0.");
+ } else {
+ Log.i(TAG, "OpenGL ES 2.0 detected.");
+ }
+
+ mGLSurfaceView = new NativeGLView(this);
+ mGLSurfaceView.setRenderer(new NativeRenderer(this));
+ setContentView(mGLSurfaceView);
+ audioPlayer = new NativeAudioPlayer();
+ }
+
+ private boolean detectOpenGLES20() {
+ ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
+ ConfigurationInfo info = am.getDeviceConfigurationInfo();
+ return info.reqGlEsVersion >= 0x20000;
+ }
+
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ Log.i(TAG, "onPause");
+ if (audioPlayer != null) {
+ audioPlayer.stop();
+ }
+ mGLSurfaceView.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.i(TAG, "onResume");
+ mGLSurfaceView.onResume();
+ if (audioPlayer != null) {
+ audioPlayer.play();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.i(TAG, "onStop");
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Log.e(TAG, "onDestroy");
+ NativeApp.shutdown();
+ audioPlayer = null;
+ mGLSurfaceView = null;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // Eat these keys, to avoid accidental exits / other screwups.
+ // Maybe there's even more we need to eat on tablets?
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ NativeApp.keyDown(1);
+ return true;
+ }
+ else if (keyCode == KeyEvent.KEYCODE_MENU) {
+ NativeApp.keyDown(2);
+ return true;
+ }
+ else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+ NativeApp.keyDown(3);
+ return true;
+ }
+ // Don't process any other keys.
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // Eat these keys, to avoid accidental exits / other screwups.
+ // Maybe there's even more we need to eat on tablets?
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ NativeApp.keyUp(1);
+ return true;
+ }
+ else if (keyCode == KeyEvent.KEYCODE_MENU) {
+ // Menu should be ignored from android 3 forwards
+ NativeApp.keyUp(2);
+ return true;
+ }
+ else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+ // Search probably should also be ignored.
+ NativeApp.keyUp(3);
+ return true;
+ }
+ return false;
+ }
+
+ // Prevent destroying and recreating the main activity when the device rotates etc,
+ // since this would stop the sound.
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Ignore orientation change
+ super.onConfigurationChanged(newConfig);
+ }
+
+ public void processCommand(String command, String params) {
+ if (command.equals("launchBrowser")) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(params));
+ startActivity(i);
+ } else if (command.equals("launchEmail")) {
+ Intent send = new Intent(Intent.ACTION_SENDTO);
+ String uriText;
+ uriText = "mailto:email@gmail.com" +
+ "?subject=Rollfish is..." +
+ "&body=great! Or?";
+ uriText = uriText.replace(" ", "%20");
+ Uri uri = Uri.parse(uriText);
+ send.setData(uri);
+ startActivity(Intent.createChooser(send, "E-mail Henrik!"));
+ } else if (command.equals("launchMarket")) {
+ // http://stackoverflow.com/questions/3442366/android-link-to-market-from-inside-another-app
+ // http://developer.android.com/guide/publishing/publishing.html#marketintent
+ } else if (command.equals("toast")) {
+ Toast toast = Toast.makeText(this, params, 2000);
+ toast.show();
+ } else {
+ Log.e(TAG, "Unsupported command " + command + " , param: " + params);
+ }
+ }
+}
diff --git a/android/src/com/turboviking/libnative/NativeApp.java b/android/src/com/turboviking/libnative/NativeApp.java
new file mode 100644
index 0000000000..5366a22c14
--- /dev/null
+++ b/android/src/com/turboviking/libnative/NativeApp.java
@@ -0,0 +1,18 @@
+package com.turboviking.libnative;
+
+public class NativeApp {
+ public static native void init(
+ int xxres, int yyres, String apkPath,
+ String dataDir, String externalDir, String installID);
+ public static native void shutdown();
+
+ public static native void keyDown(int key);
+ public static native void keyUp(int key);
+
+ // will only be called between init() and shutdown()
+ public static native void audioRender(short [] buffer);
+
+ // Sensor/input data. These are asynchronous, beware!
+ public static native void touch(int x, int y, int data);
+ public static native void accelerometer(float x, float y, float z);
+}
\ No newline at end of file