+
+// This is the file you should include from your program. It dynamically loads
+// the native_audio.so shared object and sets up the function pointers.
+
+// Do not call this if you have detected that the android version is below
+// 2.2, as it will fail miserably.
+
+// It's okay for optimalFramesPerBuffer and optimalSampleRate to be 0. Defaults will be used.
+bool AndroidAudio_Init(AndroidAudioCallback cb, std::string libraryDir, int optimalFramesPerBuffer, int optimalSampleRate);
+bool AndroidAudio_Pause();
+bool AndroidAudio_Resume();
+void AndroidAudio_Shutdown();
diff --git a/ext/native/android/proguard-project.txt b/ext/native/android/proguard-project.txt
new file mode 100644
index 0000000000..f2fe1559a2
--- /dev/null
+++ b/ext/native/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/ext/native/android/project.properties b/ext/native/android/project.properties
new file mode 100644
index 0000000000..9f2199fbd5
--- /dev/null
+++ b/ext/native/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-22
+android.library=true
diff --git a/ext/native/android/res/drawable-hdpi/ic_launcher.png b/ext/native/android/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..96a442e5b8
Binary files /dev/null and b/ext/native/android/res/drawable-hdpi/ic_launcher.png differ
diff --git a/ext/native/android/res/drawable-ldpi/ic_launcher.png b/ext/native/android/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000000..99238729d8
Binary files /dev/null and b/ext/native/android/res/drawable-ldpi/ic_launcher.png differ
diff --git a/ext/native/android/res/drawable-mdpi/ic_launcher.png b/ext/native/android/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..359047dfa4
Binary files /dev/null and b/ext/native/android/res/drawable-mdpi/ic_launcher.png differ
diff --git a/ext/native/android/res/drawable-xhdpi/ic_launcher.png b/ext/native/android/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..71c6d760f0
Binary files /dev/null and b/ext/native/android/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/AudioFocusChangeListener.java b/ext/native/android/src/com/henrikrydgard/libnative/AudioFocusChangeListener.java
new file mode 100644
index 0000000000..1415e704ff
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/AudioFocusChangeListener.java
@@ -0,0 +1,26 @@
+package com.henrikrydgard.libnative;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+
+public class AudioFocusChangeListener implements OnAudioFocusChangeListener{
+ // not used right now, but we may need to use it sometime. So just store it
+ // for now.
+ private boolean hasAudioFocus = false;
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange){
+ case AudioManager.AUDIOFOCUS_GAIN:
+ hasAudioFocus = true;
+ break;
+
+ case AudioManager.AUDIOFOCUS_LOSS:
+ hasAudioFocus = false;
+ break;
+ }
+ }
+
+ public boolean hasAudioFocus() {
+ return hasAudioFocus;
+ }
+}
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/InputDeviceState.java b/ext/native/android/src/com/henrikrydgard/libnative/InputDeviceState.java
new file mode 100644
index 0000000000..f572c44dee
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/InputDeviceState.java
@@ -0,0 +1,93 @@
+package com.henrikrydgard.libnative;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.InputDevice.MotionRange;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
+public class InputDeviceState {
+ private static final String TAG = "InputDeviceState";
+
+ private static final int deviceId = NativeApp.DEVICE_ID_PAD_0;
+
+ private InputDevice mDevice;
+ private int[] mAxes;
+
+ InputDevice getDevice() { return mDevice; }
+
+ @TargetApi(19)
+ void logAdvanced(InputDevice device) {
+ Log.i(TAG, "Vendor ID:" + device.getVendorId() + " productId: " + device.getProductId());
+ }
+
+ public InputDeviceState(InputDevice device) {
+ mDevice = device;
+ int numAxes = 0;
+ for (MotionRange range : device.getMotionRanges()) {
+ if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+ numAxes += 1;
+ }
+ }
+
+ mAxes = new int[numAxes];
+
+ int i = 0;
+ for (MotionRange range : device.getMotionRanges()) {
+ if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+ mAxes[i++] = range.getAxis();
+ }
+ }
+
+ Log.i(TAG, "Registering input device with " + numAxes + " axes: " + device.getName());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ logAdvanced(device);
+ }
+ NativeApp.sendMessage("inputDeviceConnected", device.getName());
+ }
+
+ public static float ProcessAxis(InputDevice.MotionRange range, float axisvalue) {
+ float absaxisvalue = Math.abs(axisvalue);
+ float deadzone = range.getFlat();
+ if (absaxisvalue <= deadzone) {
+ return 0.0f;
+ }
+ float normalizedvalue;
+ if (axisvalue < 0.0f) {
+ normalizedvalue = absaxisvalue / range.getMin();
+ } else {
+ normalizedvalue = absaxisvalue / range.getMax();
+ }
+
+ return normalizedvalue;
+ }
+
+ public boolean onKeyDown(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ boolean repeat = event.getRepeatCount() > 0;
+ return NativeApp.keyDown(deviceId, keyCode, repeat);
+ }
+
+ public boolean onKeyUp(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ return NativeApp.keyUp(deviceId, keyCode);
+ }
+
+ public boolean onJoystickMotion(MotionEvent event) {
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) {
+ return false;
+ }
+ NativeApp.beginJoystickEvent();
+ for (int i = 0; i < mAxes.length; i++) {
+ int axisId = mAxes[i];
+ float value = event.getAxisValue(axisId);
+ // TODO: Use processAxis or move that to the C++ code
+ NativeApp.joystickAxis(deviceId, axisId, value);
+ }
+ NativeApp.endJoystickEvent();
+ return true;
+ }
+}
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/Installation.java b/ext/native/android/src/com/henrikrydgard/libnative/Installation.java
new file mode 100644
index 0000000000..dffa7cc441
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/Installation.java
@@ -0,0 +1,46 @@
+package com.henrikrydgard.libnative;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.UUID;
+
+import android.content.Context;
+
+
+public 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) {
+ // We can't even open a file for writing? Then we can't get a unique-ish installation id.
+ return "BROKENAPPUSERFILESYSTEM";
+ }
+ }
+ 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();
+ }
+}
+
\ No newline at end of file
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/MogaHack.java b/ext/native/android/src/com/henrikrydgard/libnative/MogaHack.java
new file mode 100644
index 0000000000..74bda85f79
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/MogaHack.java
@@ -0,0 +1,118 @@
+/**
+ * Mupen64PlusAE, an N64 emulator for the Android platform
+ *
+ * Copyright (C) 2013 Paul Lamb
+ *
+ * This file is part of Mupen64PlusAE.
+ *
+ * Mupen64PlusAE 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 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Mupen64PlusAE 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 Mupen64PlusAE. If
+ * not, see .
+ *
+ * Authors: Paul Lamb
+ */
+package com.henrikrydgard.libnative;
+
+import java.util.List;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.util.Log;
+
+import com.bda.controller.Controller;
+import com.bda.controller.IControllerService;
+
+/**
+ * Temporary hack for crash in MOGA library on Lollipop. This hack can be removed once MOGA fixes
+ * their library. The actual issue is caused by the use of implicit service intents, which are
+ * illegal in Lollipop, as seen in the logcat message below.
+ *
+ *
+ * {@code Service Intent must be explicit: Intent { act=com.bda.controller.IControllerService } }
+ *
+ *
+ * @see MOGA developer site
+ * @see
+ * Discussion on explicit intents
+ */
+public class MogaHack
+{
+ public static void init(Controller controller, Context context )
+ {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT)
+ {
+ boolean mIsBound = false;
+ java.lang.reflect.Field fIsBound = null;
+ android.content.ServiceConnection mServiceConnection = null;
+ java.lang.reflect.Field fServiceConnection = null;
+ try
+ {
+ Class> cMogaController = controller.getClass();
+ fIsBound = cMogaController.getDeclaredField( "mIsBound" );
+ fIsBound.setAccessible( true );
+ mIsBound = fIsBound.getBoolean( controller );
+ fServiceConnection = cMogaController.getDeclaredField( "mServiceConnection" );
+ fServiceConnection.setAccessible( true );
+ mServiceConnection = ( android.content.ServiceConnection ) fServiceConnection.get( controller );
+ }
+ catch( NoSuchFieldException e )
+ {
+ Log.e( "MogaHack", "MOGA Lollipop Hack NoSuchFieldException (get)", e );
+ }
+ catch( IllegalAccessException e )
+ {
+ Log.e( "MogaHack", "MOGA Lollipop Hack IllegalAccessException (get)", e );
+ }
+ catch( IllegalArgumentException e )
+ {
+ Log.e( "MogaHack", "MOGA Lollipop Hack IllegalArgumentException (get)", e );
+ }
+ if( ( !mIsBound ) && ( mServiceConnection != null ) )
+ {
+ // Convert implicit intent to explicit intent, see http://stackoverflow.com/a/26318757
+ Intent intent = new Intent( IControllerService.class.getName() );
+ List resolveInfos = context.getPackageManager().queryIntentServices( intent, 0 );
+ if( resolveInfos == null || resolveInfos.size() != 1 )
+ {
+ // What? this doesn't do anything.
+ // Log.e( "MogaHack", "Somebody is trying to intercept our intent. Disabling MOGA controller for security." );
+ }
+ ServiceInfo serviceInfo = resolveInfos.get( 0 ).serviceInfo;
+ String packageName = serviceInfo.packageName;
+ String className = serviceInfo.name;
+ intent.setComponent( new ComponentName( packageName, className ) );
+
+ // Start the service explicitly
+ context.startService( intent );
+ context.bindService( intent, mServiceConnection, 1 );
+ try
+ {
+ fIsBound.setBoolean( controller, true );
+ }
+ catch( IllegalAccessException e )
+ {
+ Log.e( "MogaHack", "MOGA Lollipop Hack IllegalAccessException (set)", e );
+ }
+ catch( IllegalArgumentException e )
+ {
+ Log.e( "MogaHack", "MOGA Lollipop Hack IllegalArgumentException (set)", e );
+ }
+ }
+ }
+ else
+ {
+ controller.init();
+ }
+ }
+}
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/NativeActivity.java b/ext/native/android/src/com/henrikrydgard/libnative/NativeActivity.java
new file mode 100644
index 0000000000..9bc0648411
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/NativeActivity.java
@@ -0,0 +1,875 @@
+package com.henrikrydgard.libnative;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Locale;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.DialogInterface;
+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.graphics.PixelFormat;
+import android.graphics.Point;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Vibrator;
+import android.text.InputType;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnSystemUiVisibilityChangeListener;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.Toast;
+
+public class NativeActivity extends Activity {
+ // Remember to loadLibrary your JNI .so in a static {} block
+
+ // Adjust these as necessary
+ private static String TAG = "NativeActivity";
+
+ // Allows us to skip a lot of initialization on secondary calls to onCreate.
+ private static boolean initialized = false;
+
+ // Graphics and audio interfaces
+ private NativeGLView mGLSurfaceView;
+ protected NativeRenderer nativeRenderer;
+
+ private String shortcutParam = "";
+
+ public static String runCommand;
+ public static String commandParameter;
+ public static String installID;
+
+ // Remember settings for best audio latency
+ private int optimalFramesPerBuffer;
+ private int optimalSampleRate;
+
+ // audioFocusChangeListener to listen to changes in audio state
+ private AudioFocusChangeListener audioFocusChangeListener;
+ private AudioManager audioManager;
+
+ private Vibrator vibrator;
+
+ private boolean isXperiaPlay;
+
+ // Allow for multiple connected gamepads but just consider them the same for now.
+ // Actually this is not entirely true, see the code.
+ InputDeviceState inputPlayerA;
+ InputDeviceState inputPlayerB;
+ InputDeviceState inputPlayerC;
+ String inputPlayerADesc;
+
+ // Functions for the app activity to override to change behaviour.
+
+ public boolean useLowProfileButtons() {
+ return true;
+ }
+
+ NativeRenderer getRenderer() {
+ return nativeRenderer;
+ }
+
+ @TargetApi(17)
+ private void detectOptimalAudioSettings() {
+ try {
+ optimalFramesPerBuffer = Integer.parseInt(this.audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER));
+ } catch (NumberFormatException e) {
+ // Ignore, if we can't parse it it's bogus and zero is a fine value (means we couldn't detect it).
+ }
+ try {
+ optimalSampleRate = Integer.parseInt(this.audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE));
+ } catch (NumberFormatException e) {
+ // Ignore, if we can't parse it it's bogus and zero is a fine value (means we couldn't detect it).
+ }
+ }
+
+ String getApplicationLibraryDir(ApplicationInfo application) {
+ String libdir = null;
+ try {
+ // Starting from Android 2.3, nativeLibraryDir is available:
+ Field field = ApplicationInfo.class.getField("nativeLibraryDir");
+ libdir = (String) field.get(application);
+ } catch (SecurityException e1) {
+ } catch (NoSuchFieldException e1) {
+ } catch (IllegalArgumentException e) {
+ } catch (IllegalAccessException e) {
+ }
+ if (libdir == null) {
+ // Fallback for Android < 2.3:
+ libdir = application.dataDir + "/lib";
+ }
+ return libdir;
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ void GetScreenSizeJB(Point size, boolean real) {
+ WindowManager w = getWindowManager();
+ if (real) {
+ w.getDefaultDisplay().getRealSize(size);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
+ void GetScreenSizeHC(Point size, boolean real) {
+ WindowManager w = getWindowManager();
+ if (real && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ GetScreenSizeJB(size, real);
+ } else {
+ w.getDefaultDisplay().getSize(size);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public void GetScreenSize(Point size) {
+ boolean real = useImmersive();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
+ GetScreenSizeHC(size, real);
+ } else {
+ WindowManager w = getWindowManager();
+ Display d = w.getDefaultDisplay();
+ size.x = d.getWidth();
+ size.y = d.getHeight();
+ }
+ }
+
+ public void setShortcutParam(String shortcutParam) {
+ this.shortcutParam = ((shortcutParam == null) ? "" : shortcutParam);
+ }
+
+ public void Initialize() {
+ // Initialize audio classes. Do this here since detectOptimalAudioSettings()
+ // needs audioManager
+ this.audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
+ this.audioFocusChangeListener = new AudioFocusChangeListener();
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ // Get the optimal buffer sz
+ detectOptimalAudioSettings();
+ }
+
+ // isLandscape is used to trigger GetAppInfo currently, we
+ boolean landscape = NativeApp.isLandscape();
+ Log.d(TAG, "Landscape: " + landscape);
+
+ // Get system information
+ ApplicationInfo appInfo = null;
+ PackageManager packMgmr = getPackageManager();
+ String packageName = getPackageName();
+ try {
+ appInfo = packMgmr.getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Unable to locate assets, aborting...");
+ }
+
+ int deviceType = NativeApp.DEVICE_TYPE_MOBILE;
+ UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
+ switch (uiModeManager.getCurrentModeType()) {
+ case Configuration.UI_MODE_TYPE_TELEVISION:
+ deviceType = NativeApp.DEVICE_TYPE_TV;
+ Log.i(TAG, "Running on an Android TV Device");
+ break;
+ case Configuration.UI_MODE_TYPE_DESK:
+ deviceType = NativeApp.DEVICE_TYPE_DESKTOP;
+ Log.i(TAG, "Running on an Android desktop computer (!)");
+ break;
+ // All other device types are treated the same.
+ }
+
+ isXperiaPlay = IsXperiaPlay();
+
+ String libraryDir = getApplicationLibraryDir(appInfo);
+ File sdcard = Environment.getExternalStorageDirectory();
+
+ String externalStorageDir = sdcard.getAbsolutePath();
+ String dataDir = this.getFilesDir().getAbsolutePath();
+ String apkFilePath = appInfo.sourceDir;
+
+ String model = Build.MANUFACTURER + ":" + Build.MODEL;
+ String languageRegion = Locale.getDefault().getLanguage() + "_" + Locale.getDefault().getCountry();
+
+ Point displaySize = new Point();
+ GetScreenSize(displaySize);
+ NativeApp.audioConfig(optimalFramesPerBuffer, optimalSampleRate);
+ NativeApp.init(model, deviceType, displaySize.x, displaySize.y, languageRegion, apkFilePath, dataDir, externalStorageDir, libraryDir, shortcutParam, installID, Build.VERSION.SDK_INT);
+
+ NativeApp.sendMessage("cacheDir", getCacheDir().getAbsolutePath());
+
+ // OK, config should be initialized, we can query for screen rotation.
+ if (Build.VERSION.SDK_INT >= 9) {
+ updateScreenRotation();
+ }
+
+ // Detect OpenGL support.
+ // We don't currently use this detection for anything but good to have in the log.
+ if (!detectOpenGLES20()) {
+ Log.i(TAG, "OpenGL ES 2.0 NOT detected. Things will likely go badly.");
+ } else {
+ if (detectOpenGLES30()) {
+ Log.i(TAG, "OpenGL ES 3.0 detected.");
+ }
+ else {
+ Log.i(TAG, "OpenGL ES 2.0 detected.");
+ }
+ }
+
+ vibrator = (Vibrator)getSystemService(VIBRATOR_SERVICE);
+ if (Build.VERSION.SDK_INT >= 11) {
+ checkForVibrator();
+ }
+ }
+
+ @TargetApi(9)
+ private void updateScreenRotation() {
+ // Query the native application on the desired rotation.
+ int rot = 0;
+ String rotString = NativeApp.queryConfig("screenRotation");
+ try {
+ rot = Integer.parseInt(rotString);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Invalid rotation: " + rotString);
+ return;
+ }
+ switch (rot) {
+ case 0:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ break;
+ case 1:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ break;
+ case 2:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ break;
+ case 3:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ break;
+ case 4:
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
+ break;
+ }
+ }
+
+ private boolean useImmersive() {
+ String immersive = NativeApp.queryConfig("immersiveMode");
+ return immersive.equals("1") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+ }
+
+ @SuppressLint("InlinedApi")
+ @TargetApi(14)
+ private void updateSystemUiVisibility() {
+ int flags = 0;
+ if (useLowProfileButtons()) {
+ flags |= View.SYSTEM_UI_FLAG_LOW_PROFILE;
+ }
+ if (useImmersive()) {
+ flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ }
+ if (getWindow().getDecorView() != null) {
+ getWindow().getDecorView().setSystemUiVisibility(flags);
+ } else {
+ Log.e(TAG, "updateSystemUiVisibility: decor view not yet created, ignoring");
+ }
+ }
+
+ // Need API 11 to check for existence of a vibrator? Zany.
+ @TargetApi(11)
+ public void checkForVibrator() {
+ if (Build.VERSION.SDK_INT >= 11) {
+ if (!vibrator.hasVibrator()) {
+ vibrator = null;
+ }
+ }
+ }
+
+ // Override this to scale the backbuffer (use the Android hardware scaler)
+ public void getDesiredBackbufferSize(Point sz) {
+ sz.x = 0;
+ sz.y = 0;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ installID = Installation.id(this);
+
+ if (!initialized) {
+ Initialize();
+ initialized = true;
+ }
+ // 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);
+
+ gainAudioFocus(this.audioManager, this.audioFocusChangeListener);
+ NativeApp.audioInit();
+
+ mGLSurfaceView = new NativeGLView(this);
+ nativeRenderer = new NativeRenderer(this);
+
+ Point sz = new Point();
+ getDesiredBackbufferSize(sz);
+ if (sz.x > 0) {
+ Log.i(TAG, "Requesting fixed size buffer: " + sz.x + "x" + sz.y);
+ // Auto-calculates new DPI and forwards to the correct call on mGLSurfaceView.getHolder()
+ nativeRenderer.setFixedSize(sz.x, sz.y, mGLSurfaceView);
+ }
+ mGLSurfaceView.setEGLContextClientVersion(2);
+
+ // Setup the GLSurface and ask android for the correct
+ // Number of bits for r, g, b, a, depth and stencil components
+ // The PSP only has 16-bit Z so that should be enough.
+ // Might want to change this for other apps (24-bit might be useful).
+ // Actually, we might be able to do without both stencil and depth in
+ // the back buffer, but that would kill non-buffered rendering.
+
+ // It appears some gingerbread devices blow up if you use a config chooser at all ???? (Xperia Play)
+ //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+
+ // On some (especially older devices), things blow up later (EGL_BAD_MATCH) if we don't set the format here,
+ // if we specify that we want destination alpha in the config chooser, which we do.
+ // http://grokbase.com/t/gg/android-developers/11bj40jm4w/fall-back
+
+
+ // Needed to avoid banding on Ouya?
+ if (Build.MANUFACTURER == "OUYA") {
+ mGLSurfaceView.getHolder().setFormat(PixelFormat.RGBX_8888);
+ mGLSurfaceView.setEGLConfigChooser(new NativeEGLConfigChooser());
+ }
+
+ mGLSurfaceView.setRenderer(nativeRenderer);
+ setContentView(mGLSurfaceView);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ updateSystemUiVisibility();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ setupSystemUiCallback();
+ }
+ }
+ }
+
+ @TargetApi(19)
+ void setupSystemUiCallback() {
+ getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(new OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ if (visibility == 0) {
+ updateSystemUiVisibility();
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.i(TAG, "onStop - do nothing special");
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "onDestroy");
+ mGLSurfaceView.onDestroy();
+ nativeRenderer.onDestroyed();
+ NativeApp.audioShutdown();
+ // Probably vain attempt to help the garbage collector...
+ mGLSurfaceView = null;
+ audioFocusChangeListener = null;
+ audioManager = null;
+ }
+
+ private boolean detectOpenGLES20() {
+ ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
+ ConfigurationInfo info = am.getDeviceConfigurationInfo();
+ return info.reqGlEsVersion >= 0x20000;
+ }
+
+ private boolean detectOpenGLES30() {
+ ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
+ ConfigurationInfo info = am.getDeviceConfigurationInfo();
+ return info.reqGlEsVersion >= 0x30000;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ Log.i(TAG, "onPause");
+ loseAudioFocus(this.audioManager, this.audioFocusChangeListener);
+ NativeApp.pause();
+ mGLSurfaceView.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ updateSystemUiVisibility();
+ }
+ // OK, config should be initialized, we can query for screen rotation.
+ if (Build.VERSION.SDK_INT >= 9) {
+ updateScreenRotation();
+ }
+
+ Log.i(TAG, "onResume");
+ if (mGLSurfaceView != null) {
+ mGLSurfaceView.onResume();
+ } else {
+ Log.e(TAG, "mGLSurfaceView really shouldn't be null in onResume");
+ }
+
+ gainAudioFocus(this.audioManager, this.audioFocusChangeListener);
+ NativeApp.resume();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ updateSystemUiVisibility();
+ }
+
+ Point sz = new Point();
+ getDesiredBackbufferSize(sz);
+ if (sz.x > 0) {
+ mGLSurfaceView.getHolder().setFixedSize(sz.x/2, sz.y/2);
+ }
+ }
+
+ //keep this static so we can call this even if we don't
+ //instantiate NativeAudioPlayer
+ public static void gainAudioFocus(AudioManager audioManager, AudioFocusChangeListener focusChangeListener) {
+ if (audioManager != null) {
+ audioManager.requestAudioFocus(focusChangeListener,
+ AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ }
+ }
+
+ //keep this static so we can call this even if we don't
+ //instantiate NativeAudioPlayer
+ public static void loseAudioFocus(AudioManager audioManager,AudioFocusChangeListener focusChangeListener){
+ if (audioManager != null) {
+ audioManager.abandonAudioFocus(focusChangeListener);
+ }
+ }
+
+ // We simply grab the first input device to produce an event and ignore all others that are connected.
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD)
+ private InputDeviceState getInputDeviceState(InputEvent event) {
+ InputDevice device = event.getDevice();
+ if (device == null) {
+ return null;
+ }
+ if (inputPlayerA == null) {
+ inputPlayerADesc = getInputDesc(device);
+ Log.i(TAG, "Input player A registered: desc = " + inputPlayerADesc);
+ inputPlayerA = new InputDeviceState(device);
+ }
+
+ if (inputPlayerA.getDevice() == device) {
+ return inputPlayerA;
+ }
+
+ if (inputPlayerB == null) {
+ Log.i(TAG, "Input player B registered: desc = " + getInputDesc(device));
+ inputPlayerB = new InputDeviceState(device);
+ }
+
+ if (inputPlayerB.getDevice() == device) {
+ return inputPlayerB;
+ }
+
+ if (inputPlayerC == null) {
+ Log.i(TAG, "Input player C registered");
+ inputPlayerC = new InputDeviceState(device);
+ }
+
+ if (inputPlayerC.getDevice() == device) {
+ return inputPlayerC;
+ }
+
+ return inputPlayerA;
+ }
+
+ public boolean IsXperiaPlay() {
+ return android.os.Build.MODEL.equals("R800a") || android.os.Build.MODEL.equals("R800i") || android.os.Build.MODEL.equals("R800x") || android.os.Build.MODEL.equals("R800at") || android.os.Build.MODEL.equals("SO-01D") || android.os.Build.MODEL.equals("zeus");
+ }
+
+ // We grab the keys before onKeyDown/... even see them. This is also better because it lets us
+ // distinguish devices.
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && !isXperiaPlay) {
+ InputDeviceState state = getInputDeviceState(event);
+ if (state == null) {
+ return super.dispatchKeyEvent(event);
+ }
+
+ // Let's let back and menu through to dispatchKeyEvent.
+ boolean passThrough = false;
+
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_MENU:
+ passThrough = true;
+ break;
+ default:
+ break;
+ }
+
+ // Don't passthrough back button if gamepad.
+ int sources = event.getSource();
+ switch (sources) {
+ case InputDevice.SOURCE_GAMEPAD:
+ case InputDevice.SOURCE_JOYSTICK:
+ case InputDevice.SOURCE_DPAD:
+ passThrough = false;
+ break;
+ }
+
+ if (!passThrough) {
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (state.onKeyDown(event)) {
+ return true;
+ }
+ break;
+
+ case KeyEvent.ACTION_UP:
+ if (state.onKeyUp(event)) {
+ return true;
+ }
+ break;
+ }
+ }
+ }
+
+ // Let's go through the old path (onKeyUp, onKeyDown).
+ return super.dispatchKeyEvent(event);
+ }
+
+ @TargetApi(16)
+ static public String getInputDesc(InputDevice input) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ return input.getDescriptor();
+ } else {
+ List motions = input.getMotionRanges();
+ String fakeid = "";
+ for (InputDevice.MotionRange range : motions)
+ fakeid += range.getAxis();
+ return fakeid;
+ }
+ }
+
+ @Override
+ @TargetApi(12)
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ // Log.d(TAG, "onGenericMotionEvent: " + event);
+ if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) {
+ if (Build.VERSION.SDK_INT >= 12) {
+ InputDeviceState state = getInputDeviceState(event);
+ if (state == null) {
+ Log.w(TAG, "Joystick event but failed to get input device state.");
+ return super.onGenericMotionEvent(event);
+ }
+ state.onJoystickMotion(event);
+ return true;
+ }
+ }
+
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_HOVER_MOVE:
+ // process the mouse hover movement...
+ return true;
+ case MotionEvent.ACTION_SCROLL:
+ NativeApp.mouseWheelEvent(event.getX(), event.getY());
+ return true;
+ }
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ @SuppressLint("NewApi")
+ @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?
+ boolean repeat = event.getRepeatCount() > 0;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (event.isAltPressed()) {
+ NativeApp.keyDown(0, 1004, repeat); // special custom keycode for the O button on Xperia Play
+ } else if (NativeApp.isAtTopLevel()) {
+ Log.i(TAG, "IsAtTopLevel returned true.");
+ // Pass through the back event.
+ return super.onKeyDown(keyCode, event);
+ } else {
+ NativeApp.keyDown(0, keyCode, repeat);
+ }
+ return true;
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_SEARCH:
+ NativeApp.keyDown(0, keyCode, repeat);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ // Joysticks are supported in Honeycomb MR1 and later via the onGenericMotionEvent method.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && event.getSource() == InputDevice.SOURCE_JOYSTICK) {
+ return super.onKeyDown(keyCode, event);
+ }
+ // Fall through
+ default:
+ // send the rest of the keys through.
+ // TODO: get rid of the three special cases above by adjusting the native side of the code.
+ // Log.d(TAG, "Key down: " + keyCode + ", KeyEvent: " + event);
+ return NativeApp.keyDown(0, keyCode, repeat);
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (event.isAltPressed()) {
+ NativeApp.keyUp(0, 1004); // special custom keycode
+ } else if (NativeApp.isAtTopLevel()) {
+ Log.i(TAG, "IsAtTopLevel returned true.");
+ return super.onKeyUp(keyCode, event);
+ } else {
+ NativeApp.keyUp(0, keyCode);
+ }
+ return true;
+ case KeyEvent.KEYCODE_MENU:
+ case KeyEvent.KEYCODE_SEARCH:
+ // Search probably should also be ignored. We send it to the app.
+ NativeApp.keyUp(0, keyCode);
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ // Joysticks are supported in Honeycomb MR1 and later via the onGenericMotionEvent method.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && event.getSource() == InputDevice.SOURCE_JOYSTICK) {
+ return super.onKeyUp(keyCode, event);
+ }
+ // Fall through
+ default:
+ // send the rest of the keys through.
+ // TODO: get rid of the three special cases above by adjusting the native side of the code.
+ // Log.d(TAG, "Key down: " + keyCode + ", KeyEvent: " + event);
+ return NativeApp.keyUp(0, keyCode);
+ }
+ }
+
+
+ @TargetApi(11)
+ private AlertDialog.Builder createDialogBuilderWithTheme() {
+ return new AlertDialog.Builder(this, AlertDialog.THEME_HOLO_DARK);
+ }
+
+ // The return value is sent elsewhere. TODO in java, in SendMessage in C++.
+ public void inputBox(String title, String defaultText, String defaultAction) {
+ final FrameLayout fl = new FrameLayout(this);
+ final EditText input = new EditText(this);
+ input.setGravity(Gravity.CENTER);
+
+ FrameLayout.LayoutParams editBoxLayout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
+ editBoxLayout.setMargins(2, 20, 2, 20);
+ fl.addView(input, editBoxLayout);
+
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ input.setText(defaultText);
+ input.selectAll();
+
+ AlertDialog.Builder bld = null;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
+ bld = new AlertDialog.Builder(this);
+ else
+ bld = createDialogBuilderWithTheme();
+
+ AlertDialog dlg = bld
+ .setView(fl)
+ .setTitle(title)
+ .setPositiveButton(defaultAction, new DialogInterface.OnClickListener(){
+ public void onClick(DialogInterface d, int which) {
+ NativeApp.sendMessage("inputbox_completed", input.getText().toString());
+ d.dismiss();
+ }
+ })
+ .setNegativeButton("Cancel", new DialogInterface.OnClickListener(){
+ public void onClick(DialogInterface d, int which) {
+ NativeApp.sendMessage("inputbox_failed", "");
+ d.cancel();
+ }
+ }).create();
+
+ dlg.setCancelable(true);
+ dlg.show();
+ }
+
+ public boolean processCommand(String command, String params) {
+ if (command.equals("launchBrowser")) {
+ Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(params));
+ startActivity(i);
+ return true;
+ } else if (command.equals("launchEmail")) {
+ Intent send = new Intent(Intent.ACTION_SENDTO);
+ String uriText;
+ uriText = "mailto:email@gmail.com" + "?subject=Your app is..."
+ + "&body=great! Or?";
+ uriText = uriText.replace(" ", "%20");
+ Uri uri = Uri.parse(uriText);
+ send.setData(uri);
+ startActivity(Intent.createChooser(send, "E-mail the app author!"));
+ return true;
+ } else if (command.equals("sharejpeg")) {
+ Intent share = new Intent(Intent.ACTION_SEND);
+ share.setType("image/jpeg");
+ share.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + params));
+ startActivity(Intent.createChooser(share, "Share Picture"));
+ } else if (command.equals("sharetext")) {
+ Intent sendIntent = new Intent();
+ sendIntent.setType("text/plain");
+ sendIntent.putExtra(Intent.EXTRA_TEXT, params);
+ sendIntent.setAction(Intent.ACTION_SEND);
+ startActivity(sendIntent);
+ } else if (command.equals("showTwitter")) {
+ String twitter_user_name = params;
+ try {
+ startActivity(new Intent(Intent.ACTION_VIEW,
+ Uri.parse("twitter://user?screen_name="
+ + twitter_user_name)));
+ } catch (Exception e) {
+ startActivity(new Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse("https://twitter.com/#!/" + twitter_user_name)));
+ }
+ } else if (command.equals("launchMarket")) {
+ // Don't need this, can just use launchBrowser with a market:
+ // http://stackoverflow.com/questions/3442366/android-link-to-market-from-inside-another-app
+ // http://developer.android.com/guide/publishing/publishing.html#marketintent
+ return false;
+ } else if (command.equals("toast")) {
+ Toast toast = Toast.makeText(this, params, Toast.LENGTH_SHORT);
+ toast.show();
+ Log.i(TAG, params);
+ return true;
+ } else if (command.equals("showKeyboard")) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ // No idea what the point of the ApplicationWindowToken is or if it
+ // matters where we get it from...
+ inputMethodManager.toggleSoftInputFromWindow(
+ mGLSurfaceView.getApplicationWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ return true;
+ } else if (command.equals("hideKeyboard")) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.toggleSoftInputFromWindow(
+ mGLSurfaceView.getApplicationWindowToken(),
+ InputMethodManager.SHOW_FORCED, 0);
+ return true;
+ } else if (command.equals("inputbox")) {
+ String title = "Input";
+ String defString = "";
+ String[] param = params.split(":");
+ if (param[0].length() > 0)
+ title = param[0];
+ if (param.length > 1)
+ defString = param[1];
+ Log.i(TAG, "Launching inputbox: " + title + " " + defString);
+ inputBox(title, defString, "OK");
+ return true;
+ } else if (command.equals("vibrate")) {
+ int milliseconds = -1;
+ if (params != "") {
+ try {
+ milliseconds = Integer.parseInt(params);
+ } catch (NumberFormatException e) {
+ }
+ }
+ // Special parameters to perform standard haptic feedback
+ // operations
+ // -1 = Standard keyboard press feedback
+ // -2 = Virtual key press
+ // -3 = Long press feedback
+ // Note that these three do not require the VIBRATE Android
+ // permission.
+ switch (milliseconds) {
+ case -1:
+ mGLSurfaceView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+ break;
+ case -2:
+ mGLSurfaceView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+ break;
+ case -3:
+ mGLSurfaceView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ break;
+ default:
+ if (vibrator != null) {
+ vibrator.vibrate(milliseconds);
+ }
+ break;
+ }
+ return true;
+ } else if (command.equals("finish")) {
+ finish();
+ } else if (command.equals("rotate")) {
+ if (Build.VERSION.SDK_INT >= 9) {
+ updateScreenRotation();
+ }
+ } else if (command.equals("immersive")) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ updateSystemUiVisibility();
+ }
+ } else if (command.equals("recreate")) {
+ recreate();
+ }
+ return false;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void recreate()
+ {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ {
+ super.recreate();
+ }
+ else
+ {
+ startActivity(getIntent());
+ finish();
+ }
+ }
+}
\ No newline at end of file
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/NativeApp.java b/ext/native/android/src/com/henrikrydgard/libnative/NativeApp.java
new file mode 100644
index 0000000000..a392f5b61a
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/NativeApp.java
@@ -0,0 +1,53 @@
+package com.henrikrydgard.libnative;
+
+
+// Note that the display* methods are in NativeRenderer.java
+
+public class NativeApp {
+ public final static int DEVICE_ID_DEFAULT = 0;
+ public final static int DEVICE_ID_KEYBOARD = 1;
+ public final static int DEVICE_ID_MOUSE = 2;
+ public final static int DEVICE_ID_PAD_0 = 10;
+
+ public final static int DEVICE_TYPE_MOBILE = 0;
+ public final static int DEVICE_TYPE_TV = 1;
+ public final static int DEVICE_TYPE_DESKTOP = 2;
+
+ public static native void init(String model, int deviceType, int xres, int yres, String languageRegion, String apkPath, String dataDir, String externalDir, String libraryDir, String shortcutParam, String installID, int androidVersion);
+
+ public static native void audioInit();
+ public static native void audioShutdown();
+ public static native void audioConfig(int optimalFramesPerBuffer, int optimalSampleRate);
+
+ public static native boolean isLandscape();
+ public static native boolean isAtTopLevel();
+
+ // These have Android semantics: Resume is always called on bootup, after init
+ public static native void pause();
+ public static native void resume();
+
+ // There's not really any reason to ever call shutdown as we can recover from a killed activity.
+ public static native void shutdown();
+
+ public static native boolean keyDown(int deviceId, int key, boolean isRepeat);
+ public static native boolean keyUp(int deviceId, int key);
+
+ public static native void beginJoystickEvent();
+ public static native void joystickAxis(int deviceId, int axis, float value);
+ public static native void endJoystickEvent();
+
+ public static native boolean mouseWheelEvent(float x, float y);
+
+ // will only be called between init() and shutdown()
+ public static native int audioRender(short[] buffer);
+
+ // Sensor/input data. These are asynchronous, beware!
+ public static native boolean touch(float x, float y, int data, int pointerId);
+
+ public static native boolean accelerometer(float x, float y, float z);
+
+ public static native void sendMessage(String msg, String arg);
+
+ public static native String queryConfig(String queryName);
+}
+
\ No newline at end of file
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/NativeEGLConfigChooser.java b/ext/native/android/src/com/henrikrydgard/libnative/NativeEGLConfigChooser.java
new file mode 100644
index 0000000000..0b0a62986e
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/NativeEGLConfigChooser.java
@@ -0,0 +1,206 @@
+package com.henrikrydgard.libnative;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLDisplay;
+
+import android.opengl.GLSurfaceView.EGLConfigChooser;
+import android.util.Log;
+
+public class NativeEGLConfigChooser implements EGLConfigChooser {
+ private static final String TAG = "NativeEGLConfigChooser";
+
+ private static final int EGL_OPENGL_ES2_BIT = 4;
+
+ private class ConfigAttribs {
+ EGLConfig config;
+ public int red;
+ public int green;
+ public int blue;
+ public int alpha;
+ public int stencil;
+ public int depth;
+ public int samples;
+ public void Log() {
+ Log.i(TAG, "EGLConfig: red=" + red + " green=" + green + " blue=" + blue + " alpha=" + alpha + " depth=" + depth + " stencil=" + stencil + " samples=" + samples);
+ }
+ }
+
+ int getEglConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attr) {
+ int[] value = new int[1];
+ try {
+ if (egl.eglGetConfigAttrib(display, config, attr, value))
+ return value[0];
+ else
+ return -1;
+ } catch (IllegalArgumentException e) {
+ if (config == null) {
+ Log.e(TAG, "Called getEglConfigAttrib with null config. Bad developer.");
+ } else {
+ Log.e(TAG, "Illegal argument to getEglConfigAttrib: attr=" + attr);
+ }
+ return -1;
+ }
+ }
+
+ ConfigAttribs[] getConfigAttribs(EGL10 egl, EGLDisplay display, EGLConfig[] configs) {
+ ConfigAttribs[] attr = new ConfigAttribs[configs.length];
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs cfg = new ConfigAttribs();
+ cfg.config = configs[i];
+ cfg.red = getEglConfigAttrib(egl, display, configs[i], EGL10.EGL_RED_SIZE);
+ cfg.green = getEglConfigAttrib(egl, display, configs[i], EGL10.EGL_GREEN_SIZE);
+ cfg.blue = getEglConfigAttrib(egl, display, configs[i], EGL10.EGL_BLUE_SIZE);
+ cfg.alpha = getEglConfigAttrib(egl, display, configs[i], EGL10.EGL_ALPHA_SIZE);
+ cfg.depth = getEglConfigAttrib(egl, display, configs[i], EGL10.EGL_DEPTH_SIZE);
+ cfg.stencil = getEglConfigAttrib(egl, display, configs[i], EGL10.EGL_STENCIL_SIZE);
+ cfg.samples = getEglConfigAttrib(egl, display, configs[i], EGL10.EGL_SAMPLES);
+ attr[i] = cfg;
+ }
+ return attr;
+ }
+
+ @Override
+ public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+ // The absolute minimum. We will do our best to choose a better config though.
+ int[] configSpec = {
+ EGL10.EGL_RED_SIZE, 5,
+ EGL10.EGL_GREEN_SIZE, 6,
+ EGL10.EGL_BLUE_SIZE, 5,
+ EGL10.EGL_DEPTH_SIZE, 16,
+ EGL10.EGL_STENCIL_SIZE, 0,
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
+ EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_NONE
+ };
+
+ int[] num_config = new int[1];
+ if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) {
+ throw new IllegalArgumentException("eglChooseConfig failed when counting");
+ }
+
+ int numConfigs = num_config[0];
+ Log.i(TAG, "There are " + numConfigs + " egl configs");
+ if (numConfigs <= 0) {
+ throw new IllegalArgumentException("No configs match configSpec");
+ }
+
+ EGLConfig[] eglConfigs = new EGLConfig[numConfigs];
+ if (!egl.eglChooseConfig(display, configSpec, eglConfigs, numConfigs, num_config)) {
+ throw new IllegalArgumentException("eglChooseConfig failed when retrieving");
+ }
+
+ ConfigAttribs [] configs = getConfigAttribs(egl, display, eglConfigs);
+
+ ConfigAttribs chosen = null;
+
+ // Log them all.
+ for (int i = 0; i < configs.length; i++) {
+ configs[i].Log();
+ }
+
+ // We now ignore destination alpha as a workaround for the Mali issue
+ // where we get badly composited if we use it.
+
+ // First, find our ideal configuration. Prefer depth.
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red == 8 && c.green == 8 && c.blue == 8 && c.alpha == 0 && c.stencil >= 8 && c.depth >= 24) {
+ chosen = c;
+ break;
+ }
+ }
+
+ if (chosen == null) {
+ // Then, prefer one with 20-bit depth (Tegra 3)
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red == 8 && c.green == 8 && c.blue == 8 && c.alpha == 0 && c.stencil >= 8 && c.depth >= 20) {
+ chosen = c;
+ break;
+ }
+ }
+ }
+
+ if (chosen == null) {
+ // Second, accept one with 16-bit depth.
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red == 8 && c.green == 8 && c.blue == 8 && c.alpha == 0 && c.stencil >= 8 && c.depth >= 16) {
+ chosen = c;
+ break;
+ }
+ }
+ }
+
+ if (chosen == null) {
+ // Third, accept one with no stencil.
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red == 8 && c.green == 8 && c.blue == 8 && c.alpha == 0 && c.depth >= 16) {
+ chosen = c;
+ break;
+ }
+ }
+ }
+
+ if (chosen == null) {
+ // Third, accept one with alpha but with stencil.
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red == 8 && c.green == 8 && c.blue == 8 && c.alpha == 8 && c.stencil >= 8 && c.depth >= 24) {
+ chosen = c;
+ break;
+ }
+ }
+ }
+
+ if (chosen == null) {
+ // Third, accept one with alpha but with stencil.
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red == 8 && c.green == 8 && c.blue == 8 && c.alpha == 8 && c.stencil >= 8 && c.depth >= 16) {
+ chosen = c;
+ break;
+ }
+ }
+ }
+
+ if (chosen == null) {
+ // Fourth, accept one with 16-bit color but depth and stencil required.
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red >= 5 && c.green >= 6 && c.blue >= 5 && c.depth >= 16 && c.stencil >= 8) {
+ chosen = c;
+ break;
+ }
+ }
+ }
+
+ if (chosen == null) {
+ // Fifth, accept one with 16-bit color but depth required.
+ for (int i = 0; i < configs.length; i++) {
+ ConfigAttribs c = configs[i];
+ if (c.red >= 5 && c.green >= 6 && c.blue >= 5 && c.depth >= 16) {
+ chosen = c;
+ break;
+ }
+ }
+ }
+
+ if (chosen == null) {
+ // Final, accept the first one in the list.
+ if (configs.length > 0)
+ chosen = configs[0];
+ }
+
+ if (chosen == null) {
+ throw new IllegalArgumentException("Failed to find a valid EGL config");
+ }
+
+ Log.i(TAG, "Final chosen config: ");
+ chosen.Log();
+ return chosen.config;
+ }
+
+}
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/NativeGLView.java b/ext/native/android/src/com/henrikrydgard/libnative/NativeGLView.java
new file mode 100644
index 0000000000..65046d08ee
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/NativeGLView.java
@@ -0,0 +1,235 @@
+package com.henrikrydgard.libnative;
+
+// Touch- and sensor-enabled GLSurfaceView.
+// Supports simple multitouch and pressure.
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.os.Handler;
+// import android.os.Build;
+// import android.util.Log;
+import android.util.Log;
+import android.view.MotionEvent;
+import com.bda.controller.*;
+
+public class NativeGLView extends GLSurfaceView implements SensorEventListener, ControllerListener {
+ private static String TAG = "NativeGLView";
+ private SensorManager mSensorManager;
+ private Sensor mAccelerometer;
+ private NativeActivity mActivity;
+
+ // Moga controller
+ private Controller mController = null;
+ private boolean isMogaPro = false;
+
+ public NativeGLView(NativeActivity activity) {
+ super(activity);
+ mActivity = activity;
+
+ /*// TODO: This would be nice.
+ if (Build.VERSION.SDK_INT >= 11) {
+ try {
+ Method method_setPreserveEGLContextOnPause = GLSurfaceView.class.getMethod(
+ "setPreserveEGLContextOnPause", new Class[] { Boolean.class });
+ Log.i(TAG, "Invoking setPreserveEGLContextOnPause");
+ method_setPreserveEGLContextOnPause.invoke(this, true);
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }*/
+
+ mSensorManager = (SensorManager)activity.getSystemService(Activity.SENSOR_SERVICE);
+ mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+
+ mController = Controller.getInstance(activity);
+ try {
+ MogaHack.init(mController, activity);
+ Log.i(TAG, "MOGA initialized");
+ mController.setListener(this, new Handler());
+ } catch (Exception e) {
+ Log.i(TAG, "Moga failed to initialize");
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private int getToolType(final MotionEvent ev, int pointer) {
+ return ev.getToolType(pointer);
+ }
+
+ public boolean onTouchEvent(final MotionEvent ev) {
+ boolean canReadToolType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+
+ int numTouchesHandled = 0;
+ float scaleX = (float)mActivity.getRenderer().getDpiScaleX();
+ float scaleY = (float)mActivity.getRenderer().getDpiScaleY();
+ for (int i = 0; i < ev.getPointerCount(); i++) {
+ int pid = ev.getPointerId(i);
+ int code = 0;
+
+ final int action = ev.getActionMasked();
+
+ // These code bits are now the same as the constants in input_state.h.
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (ev.getActionIndex() == i)
+ code = 2;
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ if (ev.getActionIndex() == i)
+ code = 4;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ code = 1;
+ break;
+ default:
+ break;
+ }
+
+ if (code != 0) {
+ if (canReadToolType) {
+ int tool = getToolType(ev, i);
+ code |= tool << 10; // We use the Android tool type codes
+ }
+ // Can't use || due to short circuit evaluation
+ numTouchesHandled += NativeApp.touch(scaleX * ev.getX(i), scaleY * ev.getY(i), code, pid) ? 1 : 0;
+ }
+ }
+ return numTouchesHandled > 0;
+ }
+
+ // Sensor management
+ public void onAccuracyChanged(Sensor sensor, int arg1) {
+ }
+
+ 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);
+ if (mController != null) {
+ mController.onPause();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
+ if (mController != null) {
+ mController.onResume();
+
+ // According to the docs, the Moga's state can be inconsistent here.
+ // We should do a one time poll. TODO
+ }
+ }
+
+ public void onDestroy() {
+ if (mController != null) {
+ mController.exit();
+ }
+ }
+
+ // MOGA Controller - from ControllerListener
+ @Override
+ public void onKeyEvent(KeyEvent event) {
+ // The Moga left stick doubles as a D-pad. This creates mapping conflicts so let's turn it off.
+ // Unfortunately this breaks menu navigation in PPSSPP currently but meh.
+ // This is different on Moga Pro though.
+
+ if (!isMogaPro) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ return;
+ default:
+ break;
+ }
+ }
+
+ boolean repeat = false; // Moga has no repeats?
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ NativeApp.keyDown(NativeApp.DEVICE_ID_PAD_0, event.getKeyCode(), repeat);
+ break;
+ case KeyEvent.ACTION_UP:
+ NativeApp.keyUp(NativeApp.DEVICE_ID_PAD_0, event.getKeyCode());
+ break;
+ }
+ }
+
+ // MOGA Controller - from ControllerListener
+ @Override
+ public void onMotionEvent(com.bda.controller.MotionEvent event) {
+ NativeApp.joystickAxis(NativeApp.DEVICE_ID_PAD_0, com.bda.controller.MotionEvent.AXIS_X, event.getAxisValue(com.bda.controller.MotionEvent.AXIS_X));
+ NativeApp.joystickAxis(NativeApp.DEVICE_ID_PAD_0, com.bda.controller.MotionEvent.AXIS_Y, event.getAxisValue(com.bda.controller.MotionEvent.AXIS_Y));
+ NativeApp.joystickAxis(NativeApp.DEVICE_ID_PAD_0, com.bda.controller.MotionEvent.AXIS_Z, event.getAxisValue(com.bda.controller.MotionEvent.AXIS_Z));
+ NativeApp.joystickAxis(NativeApp.DEVICE_ID_PAD_0, com.bda.controller.MotionEvent.AXIS_RZ, event.getAxisValue(com.bda.controller.MotionEvent.AXIS_RZ));
+ NativeApp.joystickAxis(NativeApp.DEVICE_ID_PAD_0, com.bda.controller.MotionEvent.AXIS_LTRIGGER, event.getAxisValue(com.bda.controller.MotionEvent.AXIS_LTRIGGER));
+ NativeApp.joystickAxis(NativeApp.DEVICE_ID_PAD_0, com.bda.controller.MotionEvent.AXIS_RTRIGGER, event.getAxisValue(com.bda.controller.MotionEvent.AXIS_RTRIGGER));
+ }
+
+ // MOGA Controller - from ControllerListener
+ @Override
+ public void onStateEvent(StateEvent state) {
+ switch (state.getState()) {
+ case StateEvent.STATE_CONNECTION:
+ switch (state.getAction()) {
+ case StateEvent.ACTION_CONNECTED:
+ Log.i(TAG, "Moga Connected");
+ if (mController.getState(Controller.STATE_CURRENT_PRODUCT_VERSION) == Controller.ACTION_VERSION_MOGA) {
+ NativeApp.sendMessage("moga", "Moga");
+ } else {
+ Log.i(TAG, "MOGA Pro detected");
+ isMogaPro = true;
+ NativeApp.sendMessage("moga", "MogaPro");
+ }
+ break;
+ case StateEvent.ACTION_CONNECTING:
+ Log.i(TAG, "Moga Connecting...");
+ break;
+ case StateEvent.ACTION_DISCONNECTED:
+ Log.i(TAG, "Moga Disconnected (or simply Not connected)");
+ NativeApp.sendMessage("moga", "");
+ break;
+ }
+ break;
+
+ case StateEvent.STATE_POWER_LOW:
+ switch (state.getAction()) {
+ case StateEvent.ACTION_TRUE:
+ Log.i(TAG, "Moga Power Low");
+ break;
+ case StateEvent.ACTION_FALSE:
+ Log.i(TAG, "Moga Power OK");
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+}
diff --git a/ext/native/android/src/com/henrikrydgard/libnative/NativeRenderer.java b/ext/native/android/src/com/henrikrydgard/libnative/NativeRenderer.java
new file mode 100644
index 0000000000..658eacc5e8
--- /dev/null
+++ b/ext/native/android/src/com/henrikrydgard/libnative/NativeRenderer.java
@@ -0,0 +1,109 @@
+package com.henrikrydgard.libnative;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.graphics.Point;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+
+public class NativeRenderer implements GLSurfaceView.Renderer {
+ private static String TAG = "NativeRenderer";
+ private NativeActivity mActivity;
+ private boolean isDark = false;
+ private int dpi;
+ private float refreshRate;
+
+ private double dpi_scale_x;
+ private double dpi_scale_y;
+
+ int last_width, last_height;
+
+ NativeRenderer(NativeActivity act) {
+ mActivity = act;
+ DisplayMetrics metrics = new DisplayMetrics();
+ Display display = act.getWindowManager().getDefaultDisplay();
+ display.getMetrics(metrics);
+ dpi = metrics.densityDpi;
+
+ refreshRate = display.getRefreshRate();
+ }
+
+ double getDpiScaleX() {
+ return dpi_scale_x;
+ }
+ double getDpiScaleY() {
+ return dpi_scale_y;
+ }
+
+ public void setDark(boolean d) {
+ isDark = d;
+ }
+
+ public void setFixedSize(int xres, int yres, GLSurfaceView surfaceView) {
+ Log.i(TAG, "Setting surface to fixed size " + xres + "x" + yres);
+ surfaceView.getHolder().setFixedSize(xres, yres);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 unused /*use GLES20*/) {
+ if (isDark) {
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+ GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_STENCIL_BUFFER_BIT);
+ } else {
+ displayRender();
+ }
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 unused, EGLConfig config) {
+ // Log.i(TAG, "onSurfaceCreated - EGL context is new or was lost");
+ // Actually, it seems that it is here we should recreate lost GL objects.
+ displayInit();
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 unused, int width, int height) {
+ Point sz = new Point();
+ mActivity.GetScreenSize(sz);
+ double actualW = sz.x;
+ double actualH = sz.y;
+ dpi_scale_x = ((double)width / (double)actualW);
+ dpi_scale_y = ((double)height / (double)actualH);
+ Log.i(TAG, "onSurfaceChanged: " + dpi_scale_x + "x" + dpi_scale_y + " (width=" + width + ", actualW=" + actualW);
+ int scaled_dpi = (int)((double)dpi * dpi_scale_x);
+ displayResize(width, height, scaled_dpi, refreshRate);
+ last_width = width;
+ last_height = height;
+ }
+
+ // Not override, it's custom.
+ public void onDestroyed() {
+ displayShutdown();
+ }
+
+ // NATIVE METHODS
+
+ // Note: This also means "device lost" and you should reload
+ // all buffered objects.
+ public native void displayInit();
+ public native void displayResize(int w, int h, int dpi, float refreshRate);
+ public native void displayRender();
+ public native void displayShutdown();
+
+ // 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() {
+ public void run() {
+ NativeRenderer.this.mActivity.processCommand(cmd, param);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/ext/native/audio/CMakeLists.txt b/ext/native/audio/CMakeLists.txt
new file mode 100644
index 0000000000..3d742499b2
--- /dev/null
+++ b/ext/native/audio/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SRCS
+ mixer.cpp
+ wav_read.cpp)
+
+set(SRCS ${SRCS})
+
+add_library(mixer STATIC ${SRCS})
+
+if(UNIX)
+ add_definitions(-fPIC)
+endif(UNIX)
diff --git a/ext/native/audio/mixer.cpp b/ext/native/audio/mixer.cpp
new file mode 100644
index 0000000000..ac621cd380
--- /dev/null
+++ b/ext/native/audio/mixer.cpp
@@ -0,0 +1,201 @@
+#include
+
+#include "audio/mixer.h"
+#include "audio/wav_read.h"
+#include "base/logging.h"
+#include "ext/stb_vorbis/stb_vorbis.h"
+#include "file/vfs.h"
+
+// TODO:
+// * Vorbis streaming playback
+
+struct ChannelEffectState {
+ // Filter state
+};
+
+enum CLIP_TYPE {
+ CT_PCM16,
+ CT_SYNTHFX,
+ CT_VORBIS,
+ // CT_PHOENIX?
+};
+
+struct Clip {
+ int type;
+
+ short *data;
+ int length;
+ int num_channels; // this is NOT stereo vs mono
+ int sample_rate;
+ int loop_start;
+ int loop_end;
+};
+
+// If current_clip == 0, the channel is free.
+
+enum ClipPlaybackState {
+ PB_STOPPED = 0,
+ PB_PLAYING = 1,
+};
+
+
+struct Channel {
+ const Clip *current_clip;
+ // Playback state
+ ClipPlaybackState state;
+ int pos;
+ PlayParams params;
+ // Effect state
+ ChannelEffectState effect_state;
+};
+
+struct Mixer {
+ Channel *channels;
+ int sample_rate;
+ int num_channels;
+ int num_fixed_channels;
+};
+
+Mixer *mixer_create(int sample_rate, int channels, int fixed_channels) {
+ Mixer *mixer = new Mixer();
+ memset(mixer, 0, sizeof(Mixer));
+ mixer->channels = new Channel[channels];
+ memset(mixer->channels, 0, sizeof(Channel) * channels);
+ mixer->sample_rate = sample_rate;
+ mixer->num_channels = channels;
+ mixer->num_fixed_channels = fixed_channels;
+ return mixer;
+}
+
+void mixer_destroy(Mixer *mixer) {
+ delete [] mixer->channels;
+ delete mixer;
+}
+
+static int get_free_channel(Mixer *mixer) {
+ int chan_with_biggest_pos = -1;
+ int biggest_pos = -1;
+ for (int i = mixer->num_fixed_channels; i < mixer->num_channels; i++) {
+ Channel *chan = &mixer->channels[i];
+ if (!chan->current_clip) {
+ return i;
+ }
+ if (chan->pos > biggest_pos) {
+ biggest_pos = chan->pos;
+ chan_with_biggest_pos = i;
+ }
+ }
+ return chan_with_biggest_pos;
+}
+
+Clip *clip_load(const char *filename) {
+ short *data;
+ int num_samples;
+ int sample_rate, num_channels;
+
+ if (!strcmp(filename + strlen(filename) - 4, ".ogg")) {
+ // Ogg file. For now, directly decompress, no streaming support.
+ uint8_t *filedata;
+ size_t size;
+ filedata = VFSReadFile(filename, &size);
+ num_samples = (int)stb_vorbis_decode_memory(filedata, (int)size, &num_channels, &data);
+ if (num_samples <= 0)
+ return NULL;
+ sample_rate = 44100;
+ ILOG("read ogg %s, length %i, rate %i", filename, num_samples, sample_rate);
+ } else {
+ // Wav file. Easy peasy.
+ data = wav_read(filename, &num_samples, &sample_rate, &num_channels);
+ if (!data) {
+ return NULL;
+ }
+ }
+
+ Clip *clip = new Clip();
+ clip->type = CT_PCM16;
+ clip->data = data;
+ clip->length = num_samples;
+ clip->num_channels = num_channels;
+ clip->sample_rate = sample_rate;
+ clip->loop_start = 0;
+ clip->loop_end = 0;
+ return clip;
+}
+
+void clip_destroy(Clip *clip) {
+ if (clip) {
+ free(clip->data);
+ delete clip;
+ } else {
+ ELOG("Can't destroy zero clip");
+ }
+}
+
+const short *clip_data(const Clip *clip)
+{
+ return clip->data;
+}
+
+size_t clip_length(const Clip *clip) {
+ return clip->length;
+}
+
+void clip_set_loop(Clip *clip, int start, int end) {
+ clip->loop_start = start;
+ clip->loop_end = end;
+}
+
+PlayParams *mixer_play_clip(Mixer *mixer, const Clip *clip, int channel) {
+ if (channel == -1) {
+ channel = get_free_channel(mixer);
+ }
+
+ Channel *chan = &mixer->channels[channel];
+ // Think about this order and make sure it's thread"safe" (not perfect but should not cause crashes).
+ chan->pos = 0;
+ chan->current_clip = clip;
+ chan->state = PB_PLAYING;
+ PlayParams *params = &chan->params;
+ params->volume = 128;
+ params->pan = 128;
+ return params;
+}
+
+void mixer_mix(Mixer *mixer, short *buffer, int num_samples) {
+ // Clear the buffer.
+ memset(buffer, 0, num_samples * sizeof(short) * 2);
+ for (int i = 0; i < mixer->num_channels; i++) {
+ Channel *chan = &mixer->channels[i];
+ if (chan->state == PB_PLAYING) {
+ const Clip *clip = chan->current_clip;
+ if (clip->type == CT_PCM16) {
+ // For now, only allow mono PCM
+ CHECK(clip->num_channels == 1);
+ if (true || chan->params.delta == 0) {
+ // Fast playback of non pitched clips
+ int cnt = num_samples;
+ if (clip->length - chan->pos < cnt) {
+ cnt = clip->length - chan->pos;
+ }
+ // TODO: Take pan into account.
+ int left_volume = chan->params.volume;
+ int right_volume = chan->params.volume;
+ // TODO: NEONize. Can also make special loops for left_volume == right_volume etc.
+ for (int s = 0; s < cnt; s++) {
+ int cdata = clip->data[chan->pos];
+ buffer[s * 2 + 0] += cdata * left_volume >> 8;
+ buffer[s * 2 + 1] += cdata * right_volume >> 8;
+ chan->pos++;
+ }
+ if (chan->pos >= clip->length) {
+ chan->state = PB_STOPPED;
+ chan->current_clip = 0;
+ break;
+ }
+ }
+ } else if (clip->type == CT_VORBIS) {
+ // For music
+ }
+ }
+ }
+}
diff --git a/ext/native/audio/mixer.h b/ext/native/audio/mixer.h
new file mode 100644
index 0000000000..69127df8a5
--- /dev/null
+++ b/ext/native/audio/mixer.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "base/basictypes.h"
+
+// Simple mixer intended for sound effects for games.
+// The clip loading code supports ogg SFX.
+
+struct Mixer;
+struct Clip;
+struct Channel;
+
+// This struct is public for easy manipulation of running channels.
+struct PlayParams {
+ uint8_t volume; // 0-255
+ uint8_t pan; // 0-255, 127 is dead center.
+ int32_t delta;
+};
+
+// Mixer
+// ==========================
+// For now, channels is required to be 2 (it specifies L/R, not mixing channels)
+Mixer *mixer_create(int sample_rate, int channels, int fixed_channels);
+void mixer_destroy(Mixer *mixer);
+
+// Buffer must be r/w.
+void mixer_mix(Mixer *mixer, short *buffer, int num_samples);
+
+
+// Clip
+// ==========================
+Clip *clip_load(const char *filename);
+void clip_destroy(Clip *clip);
+
+const short *clip_data(const Clip *clip);
+size_t clip_length(const Clip *clip);
+void clip_set_loop(int start, int end);
+
+
+// The returned PlayState pointer can be used to set the playback parameters,
+// but must not be kept around unless you are using a fixed channel.
+// Channel must be either -1 for auto assignment to a free channel, or less
+// than the number of fixed channels that the mixer was created with.
+PlayParams *mixer_play_clip(Mixer *mixer, const Clip *clip, int channel);
diff --git a/ext/native/audio/wav_read.cpp b/ext/native/audio/wav_read.cpp
new file mode 100644
index 0000000000..6a827ece36
--- /dev/null
+++ b/ext/native/audio/wav_read.cpp
@@ -0,0 +1,70 @@
+#include "base/basictypes.h"
+#include "base/logging.h"
+#include "audio/wav_read.h"
+#include "file/chunk_file.h"
+
+short *wav_read(const char *filename,
+ int *num_samples, int *sample_rate,
+ int *num_channels)
+{
+ ChunkFile cf(filename, true);
+ if (cf.failed()) {
+ WLOG("ERROR: Wave file %s could not be opened", filename);
+ return 0;
+ }
+
+ short *data = 0;
+ int samplesPerSec, avgBytesPerSec,wBlockAlign,wBytesPerSample;
+ if (cf.descend('RIFF')) {
+ cf.readInt(); //get past 'WAVE'
+ if (cf.descend('fmt ')) { //enter the format chunk
+ int temp = cf.readInt();
+ int format = temp & 0xFFFF;
+ if (format != 1) {
+ cf.ascend();
+ cf.ascend();
+ ELOG("Error - bad format");
+ return NULL;
+ }
+ *num_channels = temp >> 16;
+ samplesPerSec = cf.readInt();
+ avgBytesPerSec = cf.readInt();
+
+ temp = cf.readInt();
+ wBlockAlign = temp & 0xFFFF;
+ wBytesPerSample = temp >> 16;
+ cf.ascend();
+ // ILOG("got fmt data: %i", samplesPerSec);
+ } else {
+ ELOG("Error - no format chunk in wav");
+ cf.ascend();
+ return NULL;
+ }
+
+ if (cf.descend('data')) { //enter the data chunk
+ int numBytes = cf.getCurrentChunkSize();
+ int numSamples = numBytes / wBlockAlign;
+ data = (short *)malloc(sizeof(short) * numSamples * *num_channels);
+ *num_samples = numSamples;
+ if (wBlockAlign == 2 && *num_channels == 1) {
+ cf.readData((uint8_t *)data,numBytes);
+ } else {
+ ELOG("Error - bad blockalign or channels");
+ free(data);
+ return NULL;
+ }
+ cf.ascend();
+ } else {
+ ELOG("Error - no data chunk in wav");
+ cf.ascend();
+ return NULL;
+ }
+ cf.ascend();
+ } else {
+ ELOG("Could not descend into RIFF file");
+ return NULL;
+ }
+ *sample_rate = samplesPerSec;
+ ILOG("read wav %s, length %i, rate %i", filename, *num_samples, *sample_rate);
+ return data;
+}
diff --git a/ext/native/audio/wav_read.h b/ext/native/audio/wav_read.h
new file mode 100644
index 0000000000..1929688cbd
--- /dev/null
+++ b/ext/native/audio/wav_read.h
@@ -0,0 +1,7 @@
+#include "base/basictypes.h"
+
+// Allocates a buffer that should be freed using free().
+short *wav_read(const char *filename,
+ int *num_samples, int *sample_rate,
+ int *num_channels);
+// TODO: Non-allocating version.
diff --git a/ext/native/base/BlackberryAudio.h b/ext/native/base/BlackberryAudio.h
new file mode 100644
index 0000000000..7c604354dc
--- /dev/null
+++ b/ext/native/base/BlackberryAudio.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2013 Sacha Refshauge
+ *
+ */
+// Blackberry implementation of the framework.
+#ifndef BLACKBERRYAUDIO_H
+#define BLACKBERRYAUDIO_H
+
+#include
+#include
+#include
+
+#include "base/NativeApp.h"
+
+#define AUDIO_FREQ 44100
+#define SAMPLE_SIZE 2048
+class BlackberryAudio
+{
+public:
+ BlackberryAudio()
+ {
+ alcDevice = alcOpenDevice(NULL);
+ if (alContext = alcCreateContext(alcDevice, NULL))
+ alcMakeContextCurrent(alContext);
+ alGenSources(1, &source);
+ alGenBuffers(1, &buffer);
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ pthread_create(&thread_handle, &attr, &BlackberryAudio::staticThreadProc, this);
+ }
+ ~BlackberryAudio()
+ {
+ pthread_cancel(thread_handle);
+ alcMakeContextCurrent(NULL);
+ if (alContext)
+ {
+ alcDestroyContext(alContext);
+ alContext = NULL;
+ }
+ if (alcDevice)
+ {
+ alcCloseDevice(alcDevice);
+ alcDevice = NULL;
+ }
+ }
+ static void* staticThreadProc(void* arg)
+ {
+ return reinterpret_cast(arg)->RunAudio();
+ }
+private:
+ void* RunAudio()
+ {
+ while(true)
+ {
+ size_t frames_ready;
+ alGetSourcei(source, AL_SOURCE_STATE, &state);
+ if (state != AL_PLAYING) {
+ frames_ready = NativeMix((short*)stream, 5*SAMPLE_SIZE);
+ }
+ else
+ frames_ready = 0;
+ if (frames_ready > 0)
+ {
+ const size_t bytes_ready = frames_ready * sizeof(short) * 2;
+ alSourcei(source, AL_BUFFER, 0);
+ alBufferData(buffer, AL_FORMAT_STEREO16, stream, bytes_ready, AUDIO_FREQ);
+ alSourcei(source, AL_BUFFER, buffer);
+ alSourcePlay(source);
+ // TODO: Maybe this could get behind?
+ usleep((1000000 * SAMPLE_SIZE) / AUDIO_FREQ);
+ }
+ else
+ usleep(10000);
+ }
+ }
+ ALCdevice *alcDevice;
+ ALCcontext *alContext;
+ ALenum state;
+ ALuint buffer;
+ ALuint source;
+ char stream[20*SAMPLE_SIZE];
+ pthread_t thread_handle;
+};
+#endif
+
diff --git a/ext/native/base/BlackberryDisplay.cpp b/ext/native/base/BlackberryDisplay.cpp
new file mode 100644
index 0000000000..e4bbdcaf6d
--- /dev/null
+++ b/ext/native/base/BlackberryDisplay.cpp
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2013 Sacha Refshauge
+ *
+ */
+// Blackberry implementation of the framework.
+#include "BlackberryMain.h"
+
+const char* BlackberryMain::displayTypeString(int type)
+{
+ switch (type)
+ {
+ case SCREEN_DISPLAY_TYPE_INTERNAL:
+ return "Internal"; break;
+ case SCREEN_DISPLAY_TYPE_COMPOSITE:
+ return "Composite"; break;
+ case SCREEN_DISPLAY_TYPE_DVI:
+ return "DVI"; break;
+ case SCREEN_DISPLAY_TYPE_HDMI:
+ return "HDMI"; break;
+ case SCREEN_DISPLAY_TYPE_DISPLAYPORT:
+ return "DisplayPort"; break;
+ case SCREEN_DISPLAY_TYPE_OTHER:
+ default:
+ break;
+ }
+ return "Unknown Port";
+}
+
+void BlackberryMain::startDisplays() {
+ int num_configs;
+ EGLint attrib_list[]= {
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_DEPTH_SIZE, 24,
+ EGL_STENCIL_SIZE, 8,
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL_NONE};
+
+ const EGLint attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
+
+ screen_get_context_property_iv(screen_cxt, SCREEN_PROPERTY_DISPLAY_COUNT, &ndisplays);
+ egl_disp = (EGLDisplay*)calloc(ndisplays, sizeof(EGLDisplay));
+ egl_surf = (EGLSurface*)calloc(ndisplays, sizeof(EGLSurface));
+ displays = (dispdata_t*)calloc(ndisplays, sizeof(dispdata_t));
+ screen_win = (screen_window_t *)calloc(ndisplays, sizeof(screen_window_t ));
+ screen_dpy = (screen_display_t*)calloc(ndisplays, sizeof(screen_display_t));
+ screen_get_context_property_pv(screen_cxt, SCREEN_PROPERTY_DISPLAYS, (void **)screen_dpy);
+
+ // Common data
+ int usage = SCREEN_USAGE_ROTATION | SCREEN_USAGE_OPENGL_ES2;
+ int format = SCREEN_FORMAT_RGBX8888;
+ int sensitivity = SCREEN_SENSITIVITY_ALWAYS;
+
+ // Initialise every display
+ for (int i = 0; i < ndisplays; i++) {
+ screen_get_display_property_iv(screen_dpy[i], SCREEN_PROPERTY_TYPE, &(displays[i].type));
+ screen_get_display_property_iv(screen_dpy[i], SCREEN_PROPERTY_ATTACHED, &(displays[i].attached));
+
+ screen_create_window(&screen_win[i], screen_cxt);
+ screen_set_window_property_iv(screen_win[i], SCREEN_PROPERTY_FORMAT, &format);
+ screen_set_window_property_iv(screen_win[i], SCREEN_PROPERTY_USAGE, &usage);
+ screen_set_window_property_iv(screen_win[i], SCREEN_PROPERTY_SENSITIVITY, &sensitivity);
+ screen_set_window_property_pv(screen_win[i], SCREEN_PROPERTY_DISPLAY, (void **)&screen_dpy[i]);
+
+ egl_disp[i] = eglGetDisplay((EGLNativeDisplayType)i);
+ eglInitialize(egl_disp[i], NULL, NULL);
+ if (egl_cont == EGL_NO_CONTEXT) {
+ eglChooseConfig(egl_disp[0], attrib_list, &egl_conf, 1, &num_configs);
+ egl_cont = eglCreateContext(egl_disp[0], egl_conf, EGL_NO_CONTEXT, attributes);
+ }
+
+ fprintf(stderr, "Display %i: %s, %s\n", i, displayTypeString(displays[i].type), displays[i].attached ? "Attached" : "Detached");
+ if (displays[i].attached)
+ realiseDisplay(i);
+ }
+#ifdef ARM
+ screen_get_display_property_iv(screen_dpy[0], SCREEN_PROPERTY_DPI, &dpi); // Only internal display has DPI
+ // We only use dpi to calculate the width. Smaller aspect ratios have giant text despite high DPI.
+ dpi = dpi * (((float)displays[0].width/(float)displays[0].height) / (16.0/9.0)); // Adjust to 16:9
+#else
+ dpi = 340.0f;
+#endif
+ g_dpi_scale = 210.0f / dpi;
+ switchDisplay(screen_ui);
+}
+
+void BlackberryMain::realiseDisplay(int idx) {
+ const EGLint egl_surfaceAttr[] = { EGL_RENDER_BUFFER, EGL_BACK_BUFFER, EGL_NONE };
+ int size[2] = { atoi(getenv("WIDTH")), atoi(getenv("HEIGHT")) };
+ if (idx != 0)
+ screen_get_display_property_iv(screen_dpy[idx], SCREEN_PROPERTY_SIZE, size);
+
+ displays[idx].width = size[0];
+ displays[idx].height = size[1];
+ screen_set_window_property_iv(screen_win[idx], SCREEN_PROPERTY_BUFFER_SIZE, size);
+ screen_create_window_buffers(screen_win[idx], 2); // Double buffered
+ fprintf(stderr, "Display %i realised with %ix%i\n", idx, size[0], size[1]);
+
+ egl_surf[idx] = eglCreateWindowSurface(egl_disp[idx], egl_conf, screen_win[idx], egl_surfaceAttr);
+
+ // Only enable for devices with hardware QWERTY, 1:1 aspect ratio
+ if ((pixel_xres == pixel_yres) && displays[idx].type != SCREEN_DISPLAY_TYPE_INTERNAL)
+ {
+ screen_emu = idx;
+ if (emulating)
+ switchDisplay(idx);
+ }
+
+ displays[idx].realised = true;
+}
+
+void BlackberryMain::unrealiseDisplay(int idx) {
+ if (displays[idx].type != SCREEN_DISPLAY_TYPE_INTERNAL) // Always true, only external can unrealise
+ {
+ screen_emu = screen_ui;
+ if (emulating)
+ switchDisplay(screen_ui);
+ }
+ killDisplay(idx, false);
+ displays[idx].realised = false;
+}
+
+void BlackberryMain::switchDisplay(int idx) {
+ static int screen_curr = -1;
+ if (idx != screen_curr) {
+ pixel_xres = displays[idx].width;
+ pixel_yres = displays[idx].height;
+ dp_xres = (int)(pixel_xres * g_dpi_scale);
+ dp_yres = (int)(pixel_yres * g_dpi_scale);
+ screen_curr = idx;
+ eglMakeCurrent(egl_disp[idx], egl_surf[idx], egl_surf[idx], egl_cont);
+ }
+ if (emulating) {
+ PSP_CoreParameter().pixelWidth = pixel_xres;
+ PSP_CoreParameter().pixelHeight = pixel_yres;
+ }
+}
+
+void BlackberryMain::killDisplays() {
+ for (int i = 0; i < ndisplays; i++) {
+ killDisplay(i, true);
+ }
+ if (egl_cont != EGL_NO_CONTEXT) {
+ eglDestroyContext(egl_disp[0], egl_cont);
+ egl_cont = EGL_NO_CONTEXT;
+ }
+ free(egl_disp);
+ free(egl_surf);
+ eglReleaseThread();
+ free(screen_dpy);
+}
+
+void BlackberryMain::killDisplay(int idx, bool killContext) {
+ if (egl_disp[idx] != EGL_NO_DISPLAY) {
+ if (killContext)
+ eglMakeCurrent(egl_disp[idx], EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ if (egl_surf[idx] != EGL_NO_SURFACE) {
+ eglDestroySurface(egl_disp[idx], egl_surf[idx]);
+ egl_surf[idx] = EGL_NO_SURFACE;
+ }
+ if (killContext)
+ {
+ eglTerminate(egl_disp[idx]);
+ egl_disp[idx] = EGL_NO_DISPLAY;
+ }
+ }
+ if (killContext && screen_win[idx] != NULL) {
+ screen_destroy_window(screen_win[idx]);
+ screen_win[idx] = NULL;
+ }
+ screen_destroy_window_buffers(screen_win[idx]);
+}
+
diff --git a/ext/native/base/BlackberryMain.cpp b/ext/native/base/BlackberryMain.cpp
new file mode 100644
index 0000000000..f6bc2ece11
--- /dev/null
+++ b/ext/native/base/BlackberryMain.cpp
@@ -0,0 +1,359 @@
+/*
+ * Copyright (c) 2013 Sacha Refshauge
+ *
+ */
+// Blackberry implementation of the framework.
+
+#include
+#include
+#include
+#include
+
+#include // Get locale
+#include // Receive invocation messages
+#include "BlackberryMain.h"
+#include "base/NKCodeFromBlackberry.h"
+
+// Bad: PPSSPP includes from native
+#include "Core/System.h"
+#include "Core/Config.h"
+#include "Core/Core.h"
+#include "UI/MiscScreens.h"
+
+static bool g_quitRequested = false;
+
+// Simple implementations of System functions
+
+std::string System_GetProperty(SystemProperty prop) {
+ switch (prop) {
+ case SYSPROP_NAME: {
+ std::string name = "Blackberry:";
+#ifdef ARM
+ return name + ((pixel_xres != pixel_yres) ? "Touch" : "QWERTY");
+#else
+ return name + "Simulator";
+#endif
+ }
+ case SYSPROP_LANGREGION: {
+ char *locale = 0;
+ locale_get_locale(&locale);
+ return std::string(locale);
+ }
+ default:
+ return "";
+ }
+}
+
+int System_GetPropertyInt(SystemProperty prop) {
+ switch (prop) {
+ case SYSPROP_AUDIO_SAMPLE_RATE:
+ return 44100;
+ case SYSPROP_DISPLAY_REFRESH_RATE:
+ return 60000;
+ case SYSPROP_DEVICE_TYPE:
+ return DEVICE_TYPE_MOBILE;
+ default:
+ return -1;
+ }
+}
+
+void System_SendMessage(const char *command, const char *parameter) {
+ if (!strcmp(command, "finish")) {
+ g_quitRequested = true;
+ }
+}
+
+void SystemToast(const char *text) {
+ dialog_instance_t dialog = 0;
+ dialog_create_toast(&dialog);
+ dialog_set_toast_message_text(dialog, text);
+ dialog_set_toast_position(dialog, DIALOG_POSITION_TOP_CENTER);
+ dialog_show(dialog);
+}
+
+void ShowAd(int x, int y, bool center_x) {
+}
+
+void ShowKeyboard() {
+ virtualkeyboard_show();
+}
+
+void Vibrate(int length_ms) {
+ // Vibration: intensity strength(1-100), duration ms(0-5000)
+ // Intensity: LOW = 1, MEDIUM = 10, HIGH = 100
+ switch (length_ms) {
+ case -1: // Keyboard Tap
+ vibration_request(VIBRATION_INTENSITY_LOW, 50);
+ break;
+ case -2: // Virtual Key
+ vibration_request(VIBRATION_INTENSITY_LOW, 25);
+ break;
+ case -3: // Long Press
+ vibration_request(VIBRATION_INTENSITY_LOW, 50);
+ break;
+ default:
+ vibration_request(VIBRATION_INTENSITY_LOW, length_ms);
+ break;
+ }
+}
+
+void LaunchBrowser(const char *url)
+{
+ char* error;
+ navigator_invoke(url, &error);
+}
+
+void LaunchMarket(const char *url)
+{
+ char* error;
+ navigator_invoke(url, &error);
+}
+
+void LaunchEmail(const char *email_address)
+{
+ char* error;
+ navigator_invoke((std::string("mailto:") + email_address).c_str(), &error);
+}
+
+InputState input_state;
+
+void BlackberryMain::handleInput(screen_event_t screen_event)
+{
+ TouchInput input;
+ KeyInput key;
+ int val, buttons, pointerId;
+ int pair[2];
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_TYPE, &val);
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_SOURCE_POSITION, pair);
+
+ input_state.mouse_valid = true;
+ switch(val)
+ {
+ // Touchscreen
+ case SCREEN_EVENT_MTOUCH_TOUCH:
+ case SCREEN_EVENT_MTOUCH_RELEASE: // Up, down
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_TOUCH_ID, &pointerId);
+ input_state.pointer_down[pointerId] = (val == SCREEN_EVENT_MTOUCH_TOUCH);
+ input_state.pointer_x[pointerId] = pair[0] * g_dpi_scale;
+ input_state.pointer_y[pointerId] = pair[1] * g_dpi_scale;
+
+ input.x = pair[0] * g_dpi_scale;
+ input.y = pair[1] * g_dpi_scale;
+ input.flags = (val == SCREEN_EVENT_MTOUCH_TOUCH) ? TOUCH_DOWN : TOUCH_UP;
+ input.id = pointerId;
+ NativeTouch(input);
+ break;
+ case SCREEN_EVENT_MTOUCH_MOVE:
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_TOUCH_ID, &pointerId);
+ input_state.pointer_x[pointerId] = pair[0] * g_dpi_scale;
+ input_state.pointer_y[pointerId] = pair[1] * g_dpi_scale;
+
+ input.x = pair[0] * g_dpi_scale;
+ input.y = pair[1] * g_dpi_scale;
+ input.flags = TOUCH_MOVE;
+ input.id = pointerId;
+ NativeTouch(input);
+ break;
+ // Mouse, Simulator
+ case SCREEN_EVENT_POINTER:
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_BUTTONS, &buttons);
+ if (buttons == SCREEN_LEFT_MOUSE_BUTTON) { // Down
+ input_state.pointer_x[0] = pair[0] * g_dpi_scale;
+ input_state.pointer_y[0] = pair[1] * g_dpi_scale;
+ input_state.pointer_down[0] = true;
+
+ input.x = pair[0] * g_dpi_scale;
+ input.y = pair[1] * g_dpi_scale;
+ input.flags = TOUCH_DOWN;
+ input.id = 0;
+ NativeTouch(input);
+ } else if (input_state.pointer_down[0]) { // Up
+ input_state.pointer_x[0] = pair[0] * g_dpi_scale;
+ input_state.pointer_y[0] = pair[1] * g_dpi_scale;
+ input_state.pointer_down[0] = false;
+
+ input.x = pair[0] * g_dpi_scale;
+ input.y = pair[1] * g_dpi_scale;
+ input.flags = TOUCH_UP;
+ input.id = 0;
+ NativeTouch(input);
+ }
+ break;
+ // Keyboard
+ case SCREEN_EVENT_KEYBOARD:
+ int flags, value;
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_KEY_FLAGS, &flags);
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_KEY_SYM, &value);
+ NativeKey(KeyInput(DEVICE_ID_KEYBOARD, KeyMapRawBlackberrytoNative.find(value)->second, (flags & KEY_DOWN) ? KEY_DOWN : KEY_UP));
+ break;
+ // Gamepad
+ case SCREEN_EVENT_GAMEPAD:
+ case SCREEN_EVENT_JOYSTICK:
+ int analog0[3];
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_BUTTONS, &buttons);
+ for (int i = 0; i < 32; i++) {
+ int mask = 1 << i;
+ if ((old_buttons & mask) != (buttons & mask))
+ NativeKey(KeyInput(DEVICE_ID_PAD_0, KeyMapPadBlackberrytoNative.find(mask)->second, (buttons & mask) ? KEY_DOWN : KEY_UP));
+ }
+ if (!screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_ANALOG0, analog0)) {
+ for (int i = 0; i < 2; i++) {
+ AxisInput axis;
+ axis.axisId = JOYSTICK_AXIS_X + i;
+ // 1.2 to try to approximate the PSP's clamped rectangular range.
+ axis.value = 1.2 * analog0[i] / 128.0f;
+ if (axis.value > 1.0f) axis.value = 1.0f;
+ if (axis.value < -1.0f) axis.value = -1.0f;
+ axis.deviceId = DEVICE_ID_PAD_0;
+ axis.flags = 0;
+ NativeAxis(axis);
+ }
+ }
+ old_buttons = buttons;
+ break;
+ case SCREEN_EVENT_DISPLAY:
+ screen_display_t new_dpy = NULL;
+ screen_get_event_property_pv(screen_event, SCREEN_PROPERTY_DISPLAY, (void **)&new_dpy);
+ for (int i = 0; i < ndisplays; i++) {
+ if (new_dpy != screen_dpy[i])
+ continue;
+ int active = 0;
+ screen_get_event_property_iv(screen_event, SCREEN_PROPERTY_ATTACHED, &active);
+ if (active) {
+ int size[2];
+ screen_get_display_property_iv(screen_dpy[i], SCREEN_PROPERTY_SIZE, size);
+ if (size[0] == 0 || size[1] == 0)
+ active = 0;
+ }
+ if (active && !displays[i].attached)
+ realiseDisplay(i);
+ else if (!active && displays[i].attached && displays[i].realised)
+ unrealiseDisplay(i);
+ displays[i].attached = active;
+ }
+ break;
+ }
+}
+
+void BlackberryMain::startMain(int argc, char *argv[]) {
+ g_quitRequested = false;
+ // Receive events from window manager
+ screen_create_context(&screen_cxt, 0);
+ // Initialise Blackberry Platform Services
+ bps_initialize();
+ // TODO: Enable/disable based on setting
+ sensor_set_rate(SENSOR_TYPE_ACCELEROMETER, 25000);
+ sensor_request_events(SENSOR_TYPE_ACCELEROMETER);
+
+ net::Init();
+ startDisplays();
+ screen_request_events(screen_cxt);
+ navigator_request_events(0);
+ dialog_request_events(0);
+ vibration_request_events(0);
+ NativeInit(argc, (const char **)argv, "/accounts/1000/shared/misc/", "app/native/assets/", "BADCOFFEE");
+ NativeInitGraphics();
+ audio = new BlackberryAudio();
+ runMain();
+}
+
+void BlackberryMain::runMain() {
+ bool running = true;
+ while (running && !g_quitRequested) {
+ input_state.mouse_valid = false;
+ input_state.accelerometer_valid = false;
+ while (true) {
+ // Handle Blackberry events
+ bps_event_t *event = NULL;
+ bps_get_event(&event, 0);
+ if (event == NULL)
+ break; // Ran out of events
+ int domain = bps_event_get_domain(event);
+ if (domain == screen_get_domain()) {
+ handleInput(screen_event_get_event(event));
+ } else if (domain == navigator_get_domain()) {
+ switch(bps_event_get_code(event))
+ {
+ case NAVIGATOR_INVOKE_TARGET:
+ {
+ const navigator_invoke_invocation_t *invoke = navigator_invoke_event_get_invocation(event);
+ if(invoke) {
+ boot_filename = navigator_invoke_invocation_get_uri(invoke)+7; // Remove file://
+ }
+ }
+ break;
+ case NAVIGATOR_ORIENTATION:
+ sensor_remap_coordinates(navigator_event_get_orientation_angle(event));
+ break;
+ case NAVIGATOR_WINDOW_STATE:
+ Core_NotifyWindowHidden(navigator_event_get_window_state(event) != NAVIGATOR_WINDOW_FULLSCREEN);
+ break;
+ case NAVIGATOR_BACK:
+ case NAVIGATOR_SWIPE_DOWN:
+ NativeKey(KeyInput(DEVICE_ID_KEYBOARD, NKCODE_BACK, KEY_DOWN));
+ break;
+ case NAVIGATOR_EXIT:
+ return;
+ }
+ } else if (domain == sensor_get_domain()) {
+ if (SENSOR_ACCELEROMETER_READING == bps_event_get_code(event)) {
+ sensor_event_get_xyz(event, &(input_state.acc.y), &(input_state.acc.x), &(input_state.acc.z));
+ AxisInput axis;
+ axis.deviceId = DEVICE_ID_ACCELEROMETER;
+ axis.flags = 0;
+
+ axis.axisId = JOYSTICK_AXIS_ACCELEROMETER_X;
+ axis.value = input_state.acc.x;
+ NativeAxis(axis);
+
+ axis.axisId = JOYSTICK_AXIS_ACCELEROMETER_Y;
+ axis.value = input_state.acc.y;
+ NativeAxis(axis);
+
+ axis.axisId = JOYSTICK_AXIS_ACCELEROMETER_Z;
+ axis.value = input_state.acc.z;
+ NativeAxis(axis);
+ }
+ }
+ }
+ UpdateInputState(&input_state);
+ // Work in Progress
+ // Currently: Render to HDMI port (eg. 1080p) when in game. Render to device when in menu.
+ // Idea: Render to all displays. Controls go to internal, game goes to external(s).
+ if (GetUIState() == UISTATE_INGAME && !emulating) {
+ emulating = true;
+ switchDisplay(screen_emu);
+ if (g_Config.iShowFPSCounter == 4) {
+ int options = SCREEN_DEBUG_STATISTICS;
+ screen_set_window_property_iv(screen_win[0], SCREEN_PROPERTY_DEBUG, &options);
+ }
+ } else if (GetUIState() != UISTATE_INGAME && emulating) {
+ emulating = false;
+ switchDisplay(screen_ui);
+ }
+ time_update();
+ UpdateRunLoop();
+ // This handles VSync
+ if (emulating)
+ eglSwapBuffers(egl_disp[screen_emu], egl_surf[screen_emu]);
+ else
+ eglSwapBuffers(egl_disp[screen_ui], egl_surf[screen_ui]);
+ }
+}
+
+void BlackberryMain::endMain() {
+ screen_stop_events(screen_cxt);
+ bps_shutdown();
+ NativeShutdownGraphics();
+ delete audio;
+ NativeShutdown();
+ killDisplays();
+ net::Shutdown();
+ screen_destroy_context(screen_cxt);
+}
+
+// Entry Point
+int main(int argc, char *argv[]) {
+ delete new BlackberryMain(argc, argv);
+ return 0;
+}
diff --git a/ext/native/base/BlackberryMain.h b/ext/native/base/BlackberryMain.h
new file mode 100644
index 0000000000..fe4af41d4c
--- /dev/null
+++ b/ext/native/base/BlackberryMain.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2013 Sacha Refshauge
+ *
+ */
+// Blackberry implementation of the framework.
+#ifndef BLACKBERRYMAIN_H
+#define BLACKBERRYMAIN_H
+
+// Blackberry specific
+#include // Blackberry Platform Services
+#include // Blackberry Window Manager
+#include // Invoke Service
+#include // Keyboard Service
+#include // Accelerometer
+#include // Dialog Service (Toast=BB10)
+#include // Vibrate Service (BB10)
+#include "sys/keycodes.h"
+#include "input/keycodes.h"
+
+// Display
+#include
+#include
+#include
+#include
+#include "Core/System.h"
+
+// Native
+#include "base/timeutil.h"
+#include "gfx_es2/glsl_program.h"
+#include "file/zip_read.h"
+#include "base/NativeApp.h"
+#include "input/input_state.h"
+#include "net/resolve.h"
+#include "base/display.h"
+
+#include "BlackberryAudio.h"
+
+struct dispdata_t {
+ int attached;
+ int type;
+ bool realised;
+ int width, height;
+};
+
+class BlackberryMain
+{
+public:
+ BlackberryMain(int argc, char *argv[]) :
+ emulating(false),
+ screen_ui(0), screen_emu(0),
+ old_buttons(0),
+ egl_cont(EGL_NO_CONTEXT)
+ {
+ startMain(argc, argv);
+ }
+ ~BlackberryMain() {
+ endMain();
+ }
+ void startMain(int argc, char *argv[]);
+
+private:
+ void runMain();
+ void endMain();
+
+ void handleInput(screen_event_t screen_event);
+
+ const char* displayTypeString(int type);
+ void startDisplays();
+ void* startDisplay(int idx);
+ void realiseDisplay(int idx);
+ void unrealiseDisplay(int idx);
+ void switchDisplay(int idx);
+ void killDisplays();
+ void killDisplay(int idx, bool killContext);
+
+ BlackberryAudio* audio;
+ dispdata_t *displays;
+ int dpi;
+ int ndisplays;
+ int screen_ui, screen_emu;
+ bool emulating;
+ int old_buttons;
+ EGLDisplay* egl_disp;
+ EGLSurface* egl_surf;
+ EGLContext egl_cont;
+ EGLConfig egl_conf;
+ screen_context_t screen_cxt;
+ screen_display_t *screen_dpy;
+ screen_window_t *screen_win;
+};
+
+#endif
+
diff --git a/ext/native/base/CMakeLists.txt b/ext/native/base/CMakeLists.txt
new file mode 100644
index 0000000000..54b0cd67f8
--- /dev/null
+++ b/ext/native/base/CMakeLists.txt
@@ -0,0 +1,20 @@
+set(SRCS
+ colorutil.cpp
+ timeutil.cpp
+ ../thread/threadutil.cpp
+ error_context.cpp
+ display.cpp
+ buffer.cpp
+ backtrace.cpp)
+
+add_library(base STATIC ${SRCS})
+
+if(UNIX)
+ add_definitions(-fPIC)
+endif(UNIX)
+
+add_library(timeutil STATIC timeutil.cpp)
+
+if(UNIX)
+ add_definitions(-fPIC)
+endif(UNIX)
diff --git a/ext/native/base/NKCodeFromBlackberry.h b/ext/native/base/NKCodeFromBlackberry.h
new file mode 100644
index 0000000000..d32f39bd9e
--- /dev/null
+++ b/ext/native/base/NKCodeFromBlackberry.h
@@ -0,0 +1,100 @@
+#include