0a2e5d87e31c8243d18db586ea760bd7a38f9fa7
[platform/upstream/SDL.git] / android-project / src / org / libsdl / app / SDLActivity.java
1 package org.libsdl.app;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Collections;
8 import java.util.Comparator;
9 import java.util.List;
10 import java.lang.reflect.Method;
11
12 import android.app.*;
13 import android.content.*;
14 import android.text.InputType;
15 import android.view.*;
16 import android.view.inputmethod.BaseInputConnection;
17 import android.view.inputmethod.EditorInfo;
18 import android.view.inputmethod.InputConnection;
19 import android.view.inputmethod.InputMethodManager;
20 import android.widget.AbsoluteLayout;
21 import android.widget.Button;
22 import android.widget.LinearLayout;
23 import android.widget.TextView;
24 import android.os.*;
25 import android.util.Log;
26 import android.util.SparseArray;
27 import android.graphics.*;
28 import android.graphics.drawable.Drawable;
29 import android.media.*;
30 import android.hardware.*;
31 import android.content.pm.ActivityInfo;
32
33 /**
34     SDL Activity
35 */
36 public class SDLActivity extends Activity {
37     private static final String TAG = "SDL";
38
39     // Keep track of the paused state
40     public static boolean mIsPaused, mIsSurfaceReady, mHasFocus;
41     public static boolean mExitCalledFromJava;
42
43     /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
44     public static boolean mBrokenLibraries;
45
46     // If we want to separate mouse and touch events.
47     //  This is only toggled in native code when a hint is set!
48     public static boolean mSeparateMouseAndTouch;
49
50     // Main components
51     protected static SDLActivity mSingleton;
52     protected static SDLSurface mSurface;
53     protected static View mTextEdit;
54     protected static ViewGroup mLayout;
55     protected static SDLJoystickHandler mJoystickHandler;
56
57     // This is what SDL runs in. It invokes SDL_main(), eventually
58     protected static Thread mSDLThread;
59
60     // Audio
61     protected static AudioTrack mAudioTrack;
62
63     /**
64      * This method is called by SDL before loading the native shared libraries.
65      * It can be overridden to provide names of shared libraries to be loaded.
66      * The default implementation returns the defaults. It never returns null.
67      * An array returned by a new implementation must at least contain "SDL2".
68      * Also keep in mind that the order the libraries are loaded may matter.
69      * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
70      */
71     protected String[] getLibraries() {
72         return new String[] {
73             "SDL2",
74             // "SDL2_image",
75             // "SDL2_mixer",
76             // "SDL2_net",
77             // "SDL2_ttf",
78             "main"
79         };
80     }
81
82     // Load the .so
83     public void loadLibraries() {
84        for (String lib : getLibraries()) {
85           System.loadLibrary(lib);
86        }
87     }
88
89     /**
90      * This method is called by SDL before starting the native application thread.
91      * It can be overridden to provide the arguments after the application name.
92      * The default implementation returns an empty array. It never returns null.
93      * @return arguments for the native application.
94      */
95     protected String[] getArguments() {
96         return new String[0];
97     }
98
99     public static void initialize() {
100         // The static nature of the singleton and Android quirkyness force us to initialize everything here
101         // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
102         mSingleton = null;
103         mSurface = null;
104         mTextEdit = null;
105         mLayout = null;
106         mJoystickHandler = null;
107         mSDLThread = null;
108         mAudioTrack = null;
109         mExitCalledFromJava = false;
110         mBrokenLibraries = false;
111         mIsPaused = false;
112         mIsSurfaceReady = false;
113         mHasFocus = true;
114     }
115
116     // Setup
117     @Override
118     protected void onCreate(Bundle savedInstanceState) {
119         Log.v(TAG, "Device: " + android.os.Build.DEVICE);
120         Log.v(TAG, "Model: " + android.os.Build.MODEL);
121         Log.v(TAG, "onCreate(): " + mSingleton);
122         super.onCreate(savedInstanceState);
123
124         SDLActivity.initialize();
125         // So we can call stuff from static callbacks
126         mSingleton = this;
127
128         // Load shared libraries
129         String errorMsgBrokenLib = "";
130         try {
131             loadLibraries();
132         } catch(UnsatisfiedLinkError e) {
133             System.err.println(e.getMessage());
134             mBrokenLibraries = true;
135             errorMsgBrokenLib = e.getMessage();
136         } catch(Exception e) {
137             System.err.println(e.getMessage());
138             mBrokenLibraries = true;
139             errorMsgBrokenLib = e.getMessage();
140         }
141
142         if (mBrokenLibraries)
143         {
144             AlertDialog.Builder dlgAlert  = new AlertDialog.Builder(this);
145             dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
146                   + System.getProperty("line.separator")
147                   + System.getProperty("line.separator")
148                   + "Error: " + errorMsgBrokenLib);
149             dlgAlert.setTitle("SDL Error");
150             dlgAlert.setPositiveButton("Exit",
151                 new DialogInterface.OnClickListener() {
152                     @Override
153                     public void onClick(DialogInterface dialog,int id) {
154                         // if this button is clicked, close current activity
155                         SDLActivity.mSingleton.finish();
156                     }
157                 });
158            dlgAlert.setCancelable(false);
159            dlgAlert.create().show();
160
161            return;
162         }
163
164         // Set up the surface
165         mSurface = new SDLSurface(getApplication());
166
167         if(Build.VERSION.SDK_INT >= 12) {
168             mJoystickHandler = new SDLJoystickHandler_API12();
169         }
170         else {
171             mJoystickHandler = new SDLJoystickHandler();
172         }
173
174         mLayout = new AbsoluteLayout(this);
175         mLayout.addView(mSurface);
176
177         setContentView(mLayout);
178         
179         // Get filename from "Open with" of another application
180         Intent intent = getIntent();
181
182         if (intent != null && intent.getData() != null) {
183             String filename = intent.getData().getPath();
184             if (filename != null) {
185                 Log.v(TAG, "Got filename: " + filename);
186                 SDLActivity.onNativeDropFile(filename);
187             }
188         }
189     }
190
191     // Events
192     @Override
193     protected void onPause() {
194         Log.v(TAG, "onPause()");
195         super.onPause();
196
197         if (SDLActivity.mBrokenLibraries) {
198            return;
199         }
200
201         SDLActivity.handlePause();
202     }
203
204     @Override
205     protected void onResume() {
206         Log.v(TAG, "onResume()");
207         super.onResume();
208
209         if (SDLActivity.mBrokenLibraries) {
210            return;
211         }
212
213         SDLActivity.handleResume();
214     }
215
216
217     @Override
218     public void onWindowFocusChanged(boolean hasFocus) {
219         super.onWindowFocusChanged(hasFocus);
220         Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
221
222         if (SDLActivity.mBrokenLibraries) {
223            return;
224         }
225
226         SDLActivity.mHasFocus = hasFocus;
227         if (hasFocus) {
228             SDLActivity.handleResume();
229         }
230     }
231
232     @Override
233     public void onLowMemory() {
234         Log.v(TAG, "onLowMemory()");
235         super.onLowMemory();
236
237         if (SDLActivity.mBrokenLibraries) {
238            return;
239         }
240
241         SDLActivity.nativeLowMemory();
242     }
243
244     @Override
245     protected void onDestroy() {
246         Log.v(TAG, "onDestroy()");
247
248         if (SDLActivity.mBrokenLibraries) {
249            super.onDestroy();
250            // Reset everything in case the user re opens the app
251            SDLActivity.initialize();
252            return;
253         }
254
255         // Send a quit message to the application
256         SDLActivity.mExitCalledFromJava = true;
257         SDLActivity.nativeQuit();
258
259         // Now wait for the SDL thread to quit
260         if (SDLActivity.mSDLThread != null) {
261             try {
262                 SDLActivity.mSDLThread.join();
263             } catch(Exception e) {
264                 Log.v(TAG, "Problem stopping thread: " + e);
265             }
266             SDLActivity.mSDLThread = null;
267
268             //Log.v(TAG, "Finished waiting for SDL thread");
269         }
270
271         super.onDestroy();
272         // Reset everything in case the user re opens the app
273         SDLActivity.initialize();
274     }
275
276     @Override
277     public boolean dispatchKeyEvent(KeyEvent event) {
278
279         if (SDLActivity.mBrokenLibraries) {
280            return false;
281         }
282
283         int keyCode = event.getKeyCode();
284         // Ignore certain special keys so they're handled by Android
285         if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
286             keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
287             keyCode == KeyEvent.KEYCODE_CAMERA ||
288             keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */
289             keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */
290             ) {
291             return false;
292         }
293         return super.dispatchKeyEvent(event);
294     }
295
296     /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed
297      *  is the first to be called, mIsSurfaceReady should still be set
298      *  to 'true' during the call to onPause (in a usual scenario).
299      */
300     public static void handlePause() {
301         if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) {
302             SDLActivity.mIsPaused = true;
303             SDLActivity.nativePause();
304             mSurface.handlePause();
305         }
306     }
307
308     /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready.
309      * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume
310      * every time we get one of those events, only if it comes after surfaceDestroyed
311      */
312     public static void handleResume() {
313         if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) {
314             SDLActivity.mIsPaused = false;
315             SDLActivity.nativeResume();
316             mSurface.handleResume();
317         }
318     }
319
320     /* The native thread has finished */
321     public static void handleNativeExit() {
322         SDLActivity.mSDLThread = null;
323         mSingleton.finish();
324     }
325
326
327     // Messages from the SDLMain thread
328     static final int COMMAND_CHANGE_TITLE = 1;
329     static final int COMMAND_UNUSED = 2;
330     static final int COMMAND_TEXTEDIT_HIDE = 3;
331     static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
332
333     protected static final int COMMAND_USER = 0x8000;
334
335     /**
336      * This method is called by SDL if SDL did not handle a message itself.
337      * This happens if a received message contains an unsupported command.
338      * Method can be overwritten to handle Messages in a different class.
339      * @param command the command of the message.
340      * @param param the parameter of the message. May be null.
341      * @return if the message was handled in overridden method.
342      */
343     protected boolean onUnhandledMessage(int command, Object param) {
344         return false;
345     }
346
347     /**
348      * A Handler class for Messages from native SDL applications.
349      * It uses current Activities as target (e.g. for the title).
350      * static to prevent implicit references to enclosing object.
351      */
352     protected static class SDLCommandHandler extends Handler {
353         @Override
354         public void handleMessage(Message msg) {
355             Context context = getContext();
356             if (context == null) {
357                 Log.e(TAG, "error handling message, getContext() returned null");
358                 return;
359             }
360             switch (msg.arg1) {
361             case COMMAND_CHANGE_TITLE:
362                 if (context instanceof Activity) {
363                     ((Activity) context).setTitle((String)msg.obj);
364                 } else {
365                     Log.e(TAG, "error handling message, getContext() returned no Activity");
366                 }
367                 break;
368             case COMMAND_TEXTEDIT_HIDE:
369                 if (mTextEdit != null) {
370                     mTextEdit.setVisibility(View.GONE);
371
372                     InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
373                     imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
374                 }
375                 break;
376             case COMMAND_SET_KEEP_SCREEN_ON:
377             {
378                 Window window = ((Activity) context).getWindow();
379                 if (window != null) {
380                     if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
381                         window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
382                     } else {
383                         window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
384                     }
385                 }
386                 break;
387             }
388             default:
389                 if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
390                     Log.e(TAG, "error handling message, command is " + msg.arg1);
391                 }
392             }
393         }
394     }
395
396     // Handler for the messages
397     Handler commandHandler = new SDLCommandHandler();
398
399     // Send a message from the SDLMain thread
400     boolean sendCommand(int command, Object data) {
401         Message msg = commandHandler.obtainMessage();
402         msg.arg1 = command;
403         msg.obj = data;
404         return commandHandler.sendMessage(msg);
405     }
406
407     // C functions we call
408     public static native int nativeInit(Object arguments);
409     public static native void nativeLowMemory();
410     public static native void nativeQuit();
411     public static native void nativePause();
412     public static native void nativeResume();
413     public static native void onNativeDropFile(String filename);
414     public static native void onNativeResize(int x, int y, int format, float rate);
415     public static native int onNativePadDown(int device_id, int keycode);
416     public static native int onNativePadUp(int device_id, int keycode);
417     public static native void onNativeJoy(int device_id, int axis,
418                                           float value);
419     public static native void onNativeHat(int device_id, int hat_id,
420                                           int x, int y);
421     public static native void onNativeKeyDown(int keycode);
422     public static native void onNativeKeyUp(int keycode);
423     public static native void onNativeKeyboardFocusLost();
424     public static native void onNativeMouse(int button, int action, float x, float y);
425     public static native void onNativeTouch(int touchDevId, int pointerFingerId,
426                                             int action, float x,
427                                             float y, float p);
428     public static native void onNativeAccel(float x, float y, float z);
429     public static native void onNativeSurfaceChanged();
430     public static native void onNativeSurfaceDestroyed();
431     public static native int nativeAddJoystick(int device_id, String name,
432                                                int is_accelerometer, int nbuttons,
433                                                int naxes, int nhats, int nballs);
434     public static native int nativeRemoveJoystick(int device_id);
435     public static native String nativeGetHint(String name);
436
437     /**
438      * This method is called by SDL using JNI.
439      */
440     public static boolean setActivityTitle(String title) {
441         // Called from SDLMain() thread and can't directly affect the view
442         return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
443     }
444
445     /**
446      * This method is called by SDL using JNI.
447      */
448     public static boolean sendMessage(int command, int param) {
449         return mSingleton.sendCommand(command, Integer.valueOf(param));
450     }
451
452     /**
453      * This method is called by SDL using JNI.
454      */
455     public static Context getContext() {
456         return mSingleton;
457     }
458
459     /**
460      * This method is called by SDL using JNI.
461      * @return result of getSystemService(name) but executed on UI thread.
462      */
463     public Object getSystemServiceFromUiThread(final String name) {
464         final Object lock = new Object();
465         final Object[] results = new Object[2]; // array for writable variables
466         synchronized (lock) {
467             runOnUiThread(new Runnable() {
468                 @Override
469                 public void run() {
470                     synchronized (lock) {
471                         results[0] = getSystemService(name);
472                         results[1] = Boolean.TRUE;
473                         lock.notify();
474                     }
475                 }
476             });
477             if (results[1] == null) {
478                 try {
479                     lock.wait();
480                 } catch (InterruptedException ex) {
481                     ex.printStackTrace();
482                 }
483             }
484         }
485         return results[0];
486     }
487
488     static class ShowTextInputTask implements Runnable {
489         /*
490          * This is used to regulate the pan&scan method to have some offset from
491          * the bottom edge of the input region and the top edge of an input
492          * method (soft keyboard)
493          */
494         static final int HEIGHT_PADDING = 15;
495
496         public int x, y, w, h;
497
498         public ShowTextInputTask(int x, int y, int w, int h) {
499             this.x = x;
500             this.y = y;
501             this.w = w;
502             this.h = h;
503         }
504
505         @Override
506         public void run() {
507             AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams(
508                     w, h + HEIGHT_PADDING, x, y);
509
510             if (mTextEdit == null) {
511                 mTextEdit = new DummyEdit(getContext());
512
513                 mLayout.addView(mTextEdit, params);
514             } else {
515                 mTextEdit.setLayoutParams(params);
516             }
517
518             mTextEdit.setVisibility(View.VISIBLE);
519             mTextEdit.requestFocus();
520
521             InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
522             imm.showSoftInput(mTextEdit, 0);
523         }
524     }
525
526     /**
527      * This method is called by SDL using JNI.
528      */
529     public static boolean showTextInput(int x, int y, int w, int h) {
530         // Transfer the task to the main thread as a Runnable
531         return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h));
532     }
533
534     /**
535      * This method is called by SDL using JNI.
536      */
537     public static Surface getNativeSurface() {
538         return SDLActivity.mSurface.getNativeSurface();
539     }
540
541     // Audio
542
543     /**
544      * This method is called by SDL using JNI.
545      */
546     public static int audioInit(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
547         int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
548         int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
549         int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
550
551         Log.v(TAG, "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
552
553         // Let the user pick a larger buffer if they really want -- but ye
554         // gods they probably shouldn't, the minimums are horrifyingly high
555         // latency already
556         desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
557
558         if (mAudioTrack == null) {
559             mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
560                     channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
561
562             // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
563             // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
564             // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
565
566             if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
567                 Log.e(TAG, "Failed during initialization of Audio Track");
568                 mAudioTrack = null;
569                 return -1;
570             }
571
572             mAudioTrack.play();
573         }
574
575         Log.v(TAG, "SDL audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer");
576
577         return 0;
578     }
579
580     /**
581      * This method is called by SDL using JNI.
582      */
583     public static void audioWriteShortBuffer(short[] buffer) {
584         for (int i = 0; i < buffer.length; ) {
585             int result = mAudioTrack.write(buffer, i, buffer.length - i);
586             if (result > 0) {
587                 i += result;
588             } else if (result == 0) {
589                 try {
590                     Thread.sleep(1);
591                 } catch(InterruptedException e) {
592                     // Nom nom
593                 }
594             } else {
595                 Log.w(TAG, "SDL audio: error return from write(short)");
596                 return;
597             }
598         }
599     }
600
601     /**
602      * This method is called by SDL using JNI.
603      */
604     public static void audioWriteByteBuffer(byte[] buffer) {
605         for (int i = 0; i < buffer.length; ) {
606             int result = mAudioTrack.write(buffer, i, buffer.length - i);
607             if (result > 0) {
608                 i += result;
609             } else if (result == 0) {
610                 try {
611                     Thread.sleep(1);
612                 } catch(InterruptedException e) {
613                     // Nom nom
614                 }
615             } else {
616                 Log.w(TAG, "SDL audio: error return from write(byte)");
617                 return;
618             }
619         }
620     }
621
622     /**
623      * This method is called by SDL using JNI.
624      */
625     public static void audioQuit() {
626         if (mAudioTrack != null) {
627             mAudioTrack.stop();
628             mAudioTrack = null;
629         }
630     }
631
632     // Input
633
634     /**
635      * This method is called by SDL using JNI.
636      * @return an array which may be empty but is never null.
637      */
638     public static int[] inputGetInputDeviceIds(int sources) {
639         int[] ids = InputDevice.getDeviceIds();
640         int[] filtered = new int[ids.length];
641         int used = 0;
642         for (int i = 0; i < ids.length; ++i) {
643             InputDevice device = InputDevice.getDevice(ids[i]);
644             if ((device != null) && ((device.getSources() & sources) != 0)) {
645                 filtered[used++] = device.getId();
646             }
647         }
648         return Arrays.copyOf(filtered, used);
649     }
650
651     // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
652     public static boolean handleJoystickMotionEvent(MotionEvent event) {
653         return mJoystickHandler.handleMotionEvent(event);
654     }
655
656     /**
657      * This method is called by SDL using JNI.
658      */
659     public static void pollInputDevices() {
660         if (SDLActivity.mSDLThread != null) {
661             mJoystickHandler.pollInputDevices();
662         }
663     }
664
665     // APK expansion files support
666
667     /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */
668     private Object expansionFile;
669
670     /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */
671     private Method expansionFileMethod;
672
673     /**
674      * This method was called by SDL using JNI.
675      * @deprecated because of an incorrect name
676      */
677     @Deprecated
678     public InputStream openAPKExtensionInputStream(String fileName) throws IOException {
679         return openAPKExpansionInputStream(fileName);
680     }
681
682     /**
683      * This method is called by SDL using JNI.
684      * @return an InputStream on success or null if no expansion file was used.
685      * @throws IOException on errors. Message is set for the SDL error message.
686      */
687     public InputStream openAPKExpansionInputStream(String fileName) throws IOException {
688         // Get a ZipResourceFile representing a merger of both the main and patch files
689         if (expansionFile == null) {
690             String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION");
691             if (mainHint == null) {
692                 return null; // no expansion use if no main version was set
693             }
694             String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION");
695             if (patchHint == null) {
696                 return null; // no expansion use if no patch version was set
697             }
698
699             Integer mainVersion;
700             Integer patchVersion;
701             try {
702                 mainVersion = Integer.valueOf(mainHint);
703                 patchVersion = Integer.valueOf(patchHint);
704             } catch (NumberFormatException ex) {
705                 ex.printStackTrace();
706                 throw new IOException("No valid file versions set for APK expansion files", ex);
707             }
708
709             try {
710                 // To avoid direct dependency on Google APK expansion library that is
711                 // not a part of Android SDK we access it using reflection
712                 expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport")
713                     .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class)
714                     .invoke(null, this, mainVersion, patchVersion);
715
716                 expansionFileMethod = expansionFile.getClass()
717                     .getMethod("getInputStream", String.class);
718             } catch (Exception ex) {
719                 ex.printStackTrace();
720                 expansionFile = null;
721                 expansionFileMethod = null;
722                 throw new IOException("Could not access APK expansion support library", ex);
723             }
724         }
725
726         // Get an input stream for a known file inside the expansion file ZIPs
727         InputStream fileStream;
728         try {
729             fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName);
730         } catch (Exception ex) {
731             // calling "getInputStream" failed
732             ex.printStackTrace();
733             throw new IOException("Could not open stream from APK expansion file", ex);
734         }
735
736         if (fileStream == null) {
737             // calling "getInputStream" was successful but null was returned
738             throw new IOException("Could not find path in APK expansion file");
739         }
740
741         return fileStream;
742     }
743
744     // Messagebox
745
746     /** Result of current messagebox. Also used for blocking the calling thread. */
747     protected final int[] messageboxSelection = new int[1];
748
749     /** Id of current dialog. */
750     protected int dialogs = 0;
751
752     /**
753      * This method is called by SDL using JNI.
754      * Shows the messagebox from UI thread and block calling thread.
755      * buttonFlags, buttonIds and buttonTexts must have same length.
756      * @param buttonFlags array containing flags for every button.
757      * @param buttonIds array containing id for every button.
758      * @param buttonTexts array containing text for every button.
759      * @param colors null for default or array of length 5 containing colors.
760      * @return button id or -1.
761      */
762     public int messageboxShowMessageBox(
763             final int flags,
764             final String title,
765             final String message,
766             final int[] buttonFlags,
767             final int[] buttonIds,
768             final String[] buttonTexts,
769             final int[] colors) {
770
771         messageboxSelection[0] = -1;
772
773         // sanity checks
774
775         if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
776             return -1; // implementation broken
777         }
778
779         // collect arguments for Dialog
780
781         final Bundle args = new Bundle();
782         args.putInt("flags", flags);
783         args.putString("title", title);
784         args.putString("message", message);
785         args.putIntArray("buttonFlags", buttonFlags);
786         args.putIntArray("buttonIds", buttonIds);
787         args.putStringArray("buttonTexts", buttonTexts);
788         args.putIntArray("colors", colors);
789
790         // trigger Dialog creation on UI thread
791
792         runOnUiThread(new Runnable() {
793             @Override
794             public void run() {
795                 showDialog(dialogs++, args);
796             }
797         });
798
799         // block the calling thread
800
801         synchronized (messageboxSelection) {
802             try {
803                 messageboxSelection.wait();
804             } catch (InterruptedException ex) {
805                 ex.printStackTrace();
806                 return -1;
807             }
808         }
809
810         // return selected value
811
812         return messageboxSelection[0];
813     }
814
815     @Override
816     protected Dialog onCreateDialog(int ignore, Bundle args) {
817
818         // TODO set values from "flags" to messagebox dialog
819
820         // get colors
821
822         int[] colors = args.getIntArray("colors");
823         int backgroundColor;
824         int textColor;
825         int buttonBorderColor;
826         int buttonBackgroundColor;
827         int buttonSelectedColor;
828         if (colors != null) {
829             int i = -1;
830             backgroundColor = colors[++i];
831             textColor = colors[++i];
832             buttonBorderColor = colors[++i];
833             buttonBackgroundColor = colors[++i];
834             buttonSelectedColor = colors[++i];
835         } else {
836             backgroundColor = Color.TRANSPARENT;
837             textColor = Color.TRANSPARENT;
838             buttonBorderColor = Color.TRANSPARENT;
839             buttonBackgroundColor = Color.TRANSPARENT;
840             buttonSelectedColor = Color.TRANSPARENT;
841         }
842
843         // create dialog with title and a listener to wake up calling thread
844
845         final Dialog dialog = new Dialog(this);
846         dialog.setTitle(args.getString("title"));
847         dialog.setCancelable(false);
848         dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
849             @Override
850             public void onDismiss(DialogInterface unused) {
851                 synchronized (messageboxSelection) {
852                     messageboxSelection.notify();
853                 }
854             }
855         });
856
857         // create text
858
859         TextView message = new TextView(this);
860         message.setGravity(Gravity.CENTER);
861         message.setText(args.getString("message"));
862         if (textColor != Color.TRANSPARENT) {
863             message.setTextColor(textColor);
864         }
865
866         // create buttons
867
868         int[] buttonFlags = args.getIntArray("buttonFlags");
869         int[] buttonIds = args.getIntArray("buttonIds");
870         String[] buttonTexts = args.getStringArray("buttonTexts");
871
872         final SparseArray<Button> mapping = new SparseArray<Button>();
873
874         LinearLayout buttons = new LinearLayout(this);
875         buttons.setOrientation(LinearLayout.HORIZONTAL);
876         buttons.setGravity(Gravity.CENTER);
877         for (int i = 0; i < buttonTexts.length; ++i) {
878             Button button = new Button(this);
879             final int id = buttonIds[i];
880             button.setOnClickListener(new View.OnClickListener() {
881                 @Override
882                 public void onClick(View v) {
883                     messageboxSelection[0] = id;
884                     dialog.dismiss();
885                 }
886             });
887             if (buttonFlags[i] != 0) {
888                 // see SDL_messagebox.h
889                 if ((buttonFlags[i] & 0x00000001) != 0) {
890                     mapping.put(KeyEvent.KEYCODE_ENTER, button);
891                 }
892                 if ((buttonFlags[i] & 0x00000002) != 0) {
893                     mapping.put(111, button); /* API 11: KeyEvent.KEYCODE_ESCAPE */
894                 }
895             }
896             button.setText(buttonTexts[i]);
897             if (textColor != Color.TRANSPARENT) {
898                 button.setTextColor(textColor);
899             }
900             if (buttonBorderColor != Color.TRANSPARENT) {
901                 // TODO set color for border of messagebox button
902             }
903             if (buttonBackgroundColor != Color.TRANSPARENT) {
904                 Drawable drawable = button.getBackground();
905                 if (drawable == null) {
906                     // setting the color this way removes the style
907                     button.setBackgroundColor(buttonBackgroundColor);
908                 } else {
909                     // setting the color this way keeps the style (gradient, padding, etc.)
910                     drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
911                 }
912             }
913             if (buttonSelectedColor != Color.TRANSPARENT) {
914                 // TODO set color for selected messagebox button
915             }
916             buttons.addView(button);
917         }
918
919         // create content
920
921         LinearLayout content = new LinearLayout(this);
922         content.setOrientation(LinearLayout.VERTICAL);
923         content.addView(message);
924         content.addView(buttons);
925         if (backgroundColor != Color.TRANSPARENT) {
926             content.setBackgroundColor(backgroundColor);
927         }
928
929         // add content to dialog and return
930
931         dialog.setContentView(content);
932         dialog.setOnKeyListener(new Dialog.OnKeyListener() {
933             @Override
934             public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) {
935                 Button button = mapping.get(keyCode);
936                 if (button != null) {
937                     if (event.getAction() == KeyEvent.ACTION_UP) {
938                         button.performClick();
939                     }
940                     return true; // also for ignored actions
941                 }
942                 return false;
943             }
944         });
945
946         return dialog;
947     }
948 }
949
950 /**
951     Simple nativeInit() runnable
952 */
953 class SDLMain implements Runnable {
954     @Override
955     public void run() {
956         // Runs SDL_main()
957         SDLActivity.nativeInit(SDLActivity.mSingleton.getArguments());
958
959         //Log.v("SDL", "SDL thread terminated");
960     }
961 }
962
963
964 /**
965     SDLSurface. This is what we draw on, so we need to know when it's created
966     in order to do anything useful.
967
968     Because of this, that's where we set up the SDL thread
969 */
970 class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
971     View.OnKeyListener, View.OnTouchListener, SensorEventListener  {
972
973     // Sensors
974     protected static SensorManager mSensorManager;
975     protected static Display mDisplay;
976
977     // Keep track of the surface size to normalize touch events
978     protected static float mWidth, mHeight;
979
980     // Startup
981     public SDLSurface(Context context) {
982         super(context);
983         getHolder().addCallback(this);
984
985         setFocusable(true);
986         setFocusableInTouchMode(true);
987         requestFocus();
988         setOnKeyListener(this);
989         setOnTouchListener(this);
990
991         mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
992         mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
993
994         if(Build.VERSION.SDK_INT >= 12) {
995             setOnGenericMotionListener(new SDLGenericMotionListener_API12());
996         }
997
998         // Some arbitrary defaults to avoid a potential division by zero
999         mWidth = 1.0f;
1000         mHeight = 1.0f;
1001     }
1002
1003     public void handlePause() {
1004         enableSensor(Sensor.TYPE_ACCELEROMETER, false);
1005     }
1006
1007     public void handleResume() {
1008         setFocusable(true);
1009         setFocusableInTouchMode(true);
1010         requestFocus();
1011         setOnKeyListener(this);
1012         setOnTouchListener(this);
1013         enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1014     }
1015
1016     public Surface getNativeSurface() {
1017         return getHolder().getSurface();
1018     }
1019
1020     // Called when we have a valid drawing surface
1021     @Override
1022     public void surfaceCreated(SurfaceHolder holder) {
1023         Log.v("SDL", "surfaceCreated()");
1024         holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
1025     }
1026
1027     // Called when we lose the surface
1028     @Override
1029     public void surfaceDestroyed(SurfaceHolder holder) {
1030         Log.v("SDL", "surfaceDestroyed()");
1031         // Call this *before* setting mIsSurfaceReady to 'false'
1032         SDLActivity.handlePause();
1033         SDLActivity.mIsSurfaceReady = false;
1034         SDLActivity.onNativeSurfaceDestroyed();
1035     }
1036
1037     // Called when the surface is resized
1038     @Override
1039     public void surfaceChanged(SurfaceHolder holder,
1040                                int format, int width, int height) {
1041         Log.v("SDL", "surfaceChanged()");
1042
1043         int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
1044         switch (format) {
1045         case PixelFormat.A_8:
1046             Log.v("SDL", "pixel format A_8");
1047             break;
1048         case PixelFormat.LA_88:
1049             Log.v("SDL", "pixel format LA_88");
1050             break;
1051         case PixelFormat.L_8:
1052             Log.v("SDL", "pixel format L_8");
1053             break;
1054         case PixelFormat.RGBA_4444:
1055             Log.v("SDL", "pixel format RGBA_4444");
1056             sdlFormat = 0x15421002; // SDL_PIXELFORMAT_RGBA4444
1057             break;
1058         case PixelFormat.RGBA_5551:
1059             Log.v("SDL", "pixel format RGBA_5551");
1060             sdlFormat = 0x15441002; // SDL_PIXELFORMAT_RGBA5551
1061             break;
1062         case PixelFormat.RGBA_8888:
1063             Log.v("SDL", "pixel format RGBA_8888");
1064             sdlFormat = 0x16462004; // SDL_PIXELFORMAT_RGBA8888
1065             break;
1066         case PixelFormat.RGBX_8888:
1067             Log.v("SDL", "pixel format RGBX_8888");
1068             sdlFormat = 0x16261804; // SDL_PIXELFORMAT_RGBX8888
1069             break;
1070         case PixelFormat.RGB_332:
1071             Log.v("SDL", "pixel format RGB_332");
1072             sdlFormat = 0x14110801; // SDL_PIXELFORMAT_RGB332
1073             break;
1074         case PixelFormat.RGB_565:
1075             Log.v("SDL", "pixel format RGB_565");
1076             sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565
1077             break;
1078         case PixelFormat.RGB_888:
1079             Log.v("SDL", "pixel format RGB_888");
1080             // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead?
1081             sdlFormat = 0x16161804; // SDL_PIXELFORMAT_RGB888
1082             break;
1083         default:
1084             Log.v("SDL", "pixel format unknown " + format);
1085             break;
1086         }
1087
1088         mWidth = width;
1089         mHeight = height;
1090         SDLActivity.onNativeResize(width, height, sdlFormat, mDisplay.getRefreshRate());
1091         Log.v("SDL", "Window size: " + width + "x" + height);
1092
1093  
1094         boolean skip = false;
1095         int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
1096
1097         if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
1098         {
1099             // Accept any
1100         }
1101         else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
1102         {
1103             if (mWidth > mHeight) {
1104                skip = true;
1105             }
1106         } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
1107             if (mWidth < mHeight) {
1108                skip = true;
1109             }
1110         }
1111
1112         // Special Patch for Square Resolution: Black Berry Passport
1113         if (skip) {
1114            double min = Math.min(mWidth, mHeight);
1115            double max = Math.max(mWidth, mHeight);
1116            
1117            if (max / min < 1.20) {
1118               Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
1119               skip = false;
1120            }
1121         }
1122
1123         if (skip) {
1124            Log.v("SDL", "Skip .. Surface is not ready.");
1125            return;
1126         }
1127
1128
1129         // Set mIsSurfaceReady to 'true' *before* making a call to handleResume
1130         SDLActivity.mIsSurfaceReady = true;
1131         SDLActivity.onNativeSurfaceChanged();
1132
1133
1134         if (SDLActivity.mSDLThread == null) {
1135             // This is the entry point to the C app.
1136             // Start up the C app thread and enable sensor input for the first time
1137
1138             final Thread sdlThread = new Thread(new SDLMain(), "SDLThread");
1139             enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1140             sdlThread.start();
1141
1142             // Set up a listener thread to catch when the native thread ends
1143             SDLActivity.mSDLThread = new Thread(new Runnable(){
1144                 @Override
1145                 public void run(){
1146                     try {
1147                         sdlThread.join();
1148                     }
1149                     catch(Exception e){}
1150                     finally{
1151                         // Native thread has finished
1152                         if (! SDLActivity.mExitCalledFromJava) {
1153                             SDLActivity.handleNativeExit();
1154                         }
1155                     }
1156                 }
1157             }, "SDLThreadListener");
1158             SDLActivity.mSDLThread.start();
1159         }
1160
1161         if (SDLActivity.mHasFocus) {
1162             SDLActivity.handleResume();
1163         }
1164     }
1165
1166     // Key events
1167     @Override
1168     public boolean onKey(View  v, int keyCode, KeyEvent event) {
1169         // Dispatch the different events depending on where they come from
1170         // Some SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
1171         // So, we try to process them as DPAD or GAMEPAD events first, if that fails we try them as KEYBOARD
1172
1173         if ( (event.getSource() & InputDevice.SOURCE_GAMEPAD) != 0 ||
1174                    (event.getSource() & InputDevice.SOURCE_DPAD) != 0 ) {
1175             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1176                 if (SDLActivity.onNativePadDown(event.getDeviceId(), keyCode) == 0) {
1177                     return true;
1178                 }
1179             } else if (event.getAction() == KeyEvent.ACTION_UP) {
1180                 if (SDLActivity.onNativePadUp(event.getDeviceId(), keyCode) == 0) {
1181                     return true;
1182                 }
1183             }
1184         }
1185
1186         if( (event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0) {
1187             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1188                 //Log.v("SDL", "key down: " + keyCode);
1189                 SDLActivity.onNativeKeyDown(keyCode);
1190                 return true;
1191             }
1192             else if (event.getAction() == KeyEvent.ACTION_UP) {
1193                 //Log.v("SDL", "key up: " + keyCode);
1194                 SDLActivity.onNativeKeyUp(keyCode);
1195                 return true;
1196             }
1197         }
1198
1199         return false;
1200     }
1201
1202     // Touch events
1203     @Override
1204     public boolean onTouch(View v, MotionEvent event) {
1205         /* Ref: http://developer.android.com/training/gestures/multi.html */
1206         final int touchDevId = event.getDeviceId();
1207         final int pointerCount = event.getPointerCount();
1208         int action = event.getActionMasked();
1209         int pointerFingerId;
1210         int mouseButton;
1211         int i = -1;
1212         float x,y,p;
1213
1214         // !!! FIXME: dump this SDK check after 2.0.4 ships and require API14.
1215         if (event.getSource() == InputDevice.SOURCE_MOUSE && SDLActivity.mSeparateMouseAndTouch) {
1216             if (Build.VERSION.SDK_INT < 14) {
1217                 mouseButton = 1;    // For Android==12 all mouse buttons are the left button
1218             } else {
1219                 try {
1220                     mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event);
1221                 } catch(Exception e) {
1222                     mouseButton = 1;    // oh well.
1223                 }
1224             }
1225             SDLActivity.onNativeMouse(mouseButton, action, event.getX(0), event.getY(0));
1226         } else {
1227             switch(action) {
1228                 case MotionEvent.ACTION_MOVE:
1229                     for (i = 0; i < pointerCount; i++) {
1230                         pointerFingerId = event.getPointerId(i);
1231                         x = event.getX(i) / mWidth;
1232                         y = event.getY(i) / mHeight;
1233                         p = event.getPressure(i);
1234                         if (p > 1.0f) {
1235                             // may be larger than 1.0f on some devices
1236                             // see the documentation of getPressure(i)
1237                             p = 1.0f;
1238                         }
1239                         SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1240                     }
1241                     break;
1242
1243                 case MotionEvent.ACTION_UP:
1244                 case MotionEvent.ACTION_DOWN:
1245                     // Primary pointer up/down, the index is always zero
1246                     i = 0;
1247                 case MotionEvent.ACTION_POINTER_UP:
1248                 case MotionEvent.ACTION_POINTER_DOWN:
1249                     // Non primary pointer up/down
1250                     if (i == -1) {
1251                         i = event.getActionIndex();
1252                     }
1253
1254                     pointerFingerId = event.getPointerId(i);
1255                     x = event.getX(i) / mWidth;
1256                     y = event.getY(i) / mHeight;
1257                     p = event.getPressure(i);
1258                     if (p > 1.0f) {
1259                         // may be larger than 1.0f on some devices
1260                         // see the documentation of getPressure(i)
1261                         p = 1.0f;
1262                     }
1263                     SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1264                     break;
1265
1266                 case MotionEvent.ACTION_CANCEL:
1267                     for (i = 0; i < pointerCount; i++) {
1268                         pointerFingerId = event.getPointerId(i);
1269                         x = event.getX(i) / mWidth;
1270                         y = event.getY(i) / mHeight;
1271                         p = event.getPressure(i);
1272                         if (p > 1.0f) {
1273                             // may be larger than 1.0f on some devices
1274                             // see the documentation of getPressure(i)
1275                             p = 1.0f;
1276                         }
1277                         SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
1278                     }
1279                     break;
1280
1281                 default:
1282                     break;
1283             }
1284         }
1285
1286         return true;
1287    }
1288
1289     // Sensor events
1290     public void enableSensor(int sensortype, boolean enabled) {
1291         // TODO: This uses getDefaultSensor - what if we have >1 accels?
1292         if (enabled) {
1293             mSensorManager.registerListener(this,
1294                             mSensorManager.getDefaultSensor(sensortype),
1295                             SensorManager.SENSOR_DELAY_GAME, null);
1296         } else {
1297             mSensorManager.unregisterListener(this,
1298                             mSensorManager.getDefaultSensor(sensortype));
1299         }
1300     }
1301
1302     @Override
1303     public void onAccuracyChanged(Sensor sensor, int accuracy) {
1304         // TODO
1305     }
1306
1307     @Override
1308     public void onSensorChanged(SensorEvent event) {
1309         if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
1310             float x, y;
1311             switch (mDisplay.getRotation()) {
1312                 case Surface.ROTATION_90:
1313                     x = -event.values[1];
1314                     y = event.values[0];
1315                     break;
1316                 case Surface.ROTATION_270:
1317                     x = event.values[1];
1318                     y = -event.values[0];
1319                     break;
1320                 case Surface.ROTATION_180:
1321                     x = -event.values[1];
1322                     y = -event.values[0];
1323                     break;
1324                 default:
1325                     x = event.values[0];
1326                     y = event.values[1];
1327                     break;
1328             }
1329             SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
1330                                       y / SensorManager.GRAVITY_EARTH,
1331                                       event.values[2] / SensorManager.GRAVITY_EARTH - 1);
1332         }
1333     }
1334 }
1335
1336 /* This is a fake invisible editor view that receives the input and defines the
1337  * pan&scan region
1338  */
1339 class DummyEdit extends View implements View.OnKeyListener {
1340     InputConnection ic;
1341
1342     public DummyEdit(Context context) {
1343         super(context);
1344         setFocusableInTouchMode(true);
1345         setFocusable(true);
1346         setOnKeyListener(this);
1347     }
1348
1349     @Override
1350     public boolean onCheckIsTextEditor() {
1351         return true;
1352     }
1353
1354     @Override
1355     public boolean onKey(View v, int keyCode, KeyEvent event) {
1356
1357         // This handles the hardware keyboard input
1358         if (event.isPrintingKey()) {
1359             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1360                 ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1361             }
1362             return true;
1363         }
1364
1365         if (event.getAction() == KeyEvent.ACTION_DOWN) {
1366             SDLActivity.onNativeKeyDown(keyCode);
1367             return true;
1368         } else if (event.getAction() == KeyEvent.ACTION_UP) {
1369             SDLActivity.onNativeKeyUp(keyCode);
1370             return true;
1371         }
1372
1373         return false;
1374     }
1375
1376     //
1377     @Override
1378     public boolean onKeyPreIme (int keyCode, KeyEvent event) {
1379         // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
1380         // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
1381         // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
1382         // FIXME: A more effective solution would be to change our Layout from AbsoluteLayout to Relative or Linear
1383         // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
1384         // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
1385         if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
1386             if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
1387                 SDLActivity.onNativeKeyboardFocusLost();
1388             }
1389         }
1390         return super.onKeyPreIme(keyCode, event);
1391     }
1392
1393     @Override
1394     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
1395         ic = new SDLInputConnection(this, true);
1396
1397         outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
1398         outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
1399                 | 33554432 /* API 11: EditorInfo.IME_FLAG_NO_FULLSCREEN */;
1400
1401         return ic;
1402     }
1403 }
1404
1405 class SDLInputConnection extends BaseInputConnection {
1406
1407     public SDLInputConnection(View targetView, boolean fullEditor) {
1408         super(targetView, fullEditor);
1409
1410     }
1411
1412     @Override
1413     public boolean sendKeyEvent(KeyEvent event) {
1414
1415         /*
1416          * This handles the keycodes from soft keyboard (and IME-translated
1417          * input from hardkeyboard)
1418          */
1419         int keyCode = event.getKeyCode();
1420         if (event.getAction() == KeyEvent.ACTION_DOWN) {
1421             if (event.isPrintingKey()) {
1422                 commitText(String.valueOf((char) event.getUnicodeChar()), 1);
1423             }
1424             SDLActivity.onNativeKeyDown(keyCode);
1425             return true;
1426         } else if (event.getAction() == KeyEvent.ACTION_UP) {
1427
1428             SDLActivity.onNativeKeyUp(keyCode);
1429             return true;
1430         }
1431         return super.sendKeyEvent(event);
1432     }
1433
1434     @Override
1435     public boolean commitText(CharSequence text, int newCursorPosition) {
1436
1437         nativeCommitText(text.toString(), newCursorPosition);
1438
1439         return super.commitText(text, newCursorPosition);
1440     }
1441
1442     @Override
1443     public boolean setComposingText(CharSequence text, int newCursorPosition) {
1444
1445         nativeSetComposingText(text.toString(), newCursorPosition);
1446
1447         return super.setComposingText(text, newCursorPosition);
1448     }
1449
1450     public native void nativeCommitText(String text, int newCursorPosition);
1451
1452     public native void nativeSetComposingText(String text, int newCursorPosition);
1453
1454     @Override
1455     public boolean deleteSurroundingText(int beforeLength, int afterLength) {
1456         // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
1457         if (beforeLength == 1 && afterLength == 0) {
1458             // backspace
1459             return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
1460                 && super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
1461         }
1462
1463         return super.deleteSurroundingText(beforeLength, afterLength);
1464     }
1465 }
1466
1467 /* A null joystick handler for API level < 12 devices (the accelerometer is handled separately) */
1468 class SDLJoystickHandler {
1469
1470     /**
1471      * Handles given MotionEvent.
1472      * @param event the event to be handled.
1473      * @return if given event was processed.
1474      */
1475     public boolean handleMotionEvent(MotionEvent event) {
1476         return false;
1477     }
1478
1479     /**
1480      * Handles adding and removing of input devices.
1481      */
1482     public void pollInputDevices() {
1483     }
1484 }
1485
1486 /* Actual joystick functionality available for API >= 12 devices */
1487 class SDLJoystickHandler_API12 extends SDLJoystickHandler {
1488
1489     static class SDLJoystick {
1490         public int device_id;
1491         public String name;
1492         public ArrayList<InputDevice.MotionRange> axes;
1493         public ArrayList<InputDevice.MotionRange> hats;
1494     }
1495     static class RangeComparator implements Comparator<InputDevice.MotionRange> {
1496         @Override
1497         public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
1498             return arg0.getAxis() - arg1.getAxis();
1499         }
1500     }
1501
1502     private ArrayList<SDLJoystick> mJoysticks;
1503
1504     public SDLJoystickHandler_API12() {
1505
1506         mJoysticks = new ArrayList<SDLJoystick>();
1507     }
1508
1509     @Override
1510     public void pollInputDevices() {
1511         int[] deviceIds = InputDevice.getDeviceIds();
1512         // It helps processing the device ids in reverse order
1513         // For example, in the case of the XBox 360 wireless dongle,
1514         // so the first controller seen by SDL matches what the receiver
1515         // considers to be the first controller
1516
1517         for(int i=deviceIds.length-1; i>-1; i--) {
1518             SDLJoystick joystick = getJoystick(deviceIds[i]);
1519             if (joystick == null) {
1520                 joystick = new SDLJoystick();
1521                 InputDevice joystickDevice = InputDevice.getDevice(deviceIds[i]);
1522
1523                 if ( 
1524                       (joystickDevice.getSources() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 
1525                    ||
1526                       (joystickDevice.getSources() & InputDevice.SOURCE_CLASS_BUTTON) != 0 
1527                   )
1528                 {
1529                     joystick.device_id = deviceIds[i];
1530                     joystick.name = joystickDevice.getName();
1531                     joystick.axes = new ArrayList<InputDevice.MotionRange>();
1532                     joystick.hats = new ArrayList<InputDevice.MotionRange>();
1533
1534                     List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
1535                     Collections.sort(ranges, new RangeComparator());
1536                     for (InputDevice.MotionRange range : ranges ) {
1537                         if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ) {
1538                             if (range.getAxis() == MotionEvent.AXIS_HAT_X ||
1539                                 range.getAxis() == MotionEvent.AXIS_HAT_Y) {
1540                                 joystick.hats.add(range);
1541                             }
1542                             else {
1543                                 joystick.axes.add(range);
1544                             }
1545                         }
1546                     }
1547
1548                     mJoysticks.add(joystick);
1549                     SDLActivity.nativeAddJoystick(joystick.device_id, joystick.name, 0, -1,
1550                                                   joystick.axes.size(), joystick.hats.size()/2, 0);
1551                 }
1552             }
1553         }
1554
1555         /* Check removed devices */
1556         ArrayList<Integer> removedDevices = new ArrayList<Integer>();
1557         for(int i=0; i < mJoysticks.size(); i++) {
1558             int device_id = mJoysticks.get(i).device_id;
1559             int j;
1560             for (j=0; j < deviceIds.length; j++) {
1561                 if (device_id == deviceIds[j]) break;
1562             }
1563             if (j == deviceIds.length) {
1564                 removedDevices.add(Integer.valueOf(device_id));
1565             }
1566         }
1567
1568         for(int i=0; i < removedDevices.size(); i++) {
1569             int device_id = removedDevices.get(i).intValue();
1570             SDLActivity.nativeRemoveJoystick(device_id);
1571             for (int j=0; j < mJoysticks.size(); j++) {
1572                 if (mJoysticks.get(j).device_id == device_id) {
1573                     mJoysticks.remove(j);
1574                     break;
1575                 }
1576             }
1577         }
1578     }
1579
1580     protected SDLJoystick getJoystick(int device_id) {
1581         for(int i=0; i < mJoysticks.size(); i++) {
1582             if (mJoysticks.get(i).device_id == device_id) {
1583                 return mJoysticks.get(i);
1584             }
1585         }
1586         return null;
1587     }
1588
1589     @Override
1590     public boolean handleMotionEvent(MotionEvent event) {
1591         if ( (event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) {
1592             int actionPointerIndex = event.getActionIndex();
1593             int action = event.getActionMasked();
1594             switch(action) {
1595                 case MotionEvent.ACTION_MOVE:
1596                     SDLJoystick joystick = getJoystick(event.getDeviceId());
1597                     if ( joystick != null ) {
1598                         for (int i = 0; i < joystick.axes.size(); i++) {
1599                             InputDevice.MotionRange range = joystick.axes.get(i);
1600                             /* Normalize the value to -1...1 */
1601                             float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f;
1602                             SDLActivity.onNativeJoy(joystick.device_id, i, value );
1603                         }
1604                         for (int i = 0; i < joystick.hats.size(); i+=2) {
1605                             int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) );
1606                             int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) );
1607                             SDLActivity.onNativeHat(joystick.device_id, i/2, hatX, hatY );
1608                         }
1609                     }
1610                     break;
1611                 default:
1612                     break;
1613             }
1614         }
1615         return true;
1616     }
1617 }
1618
1619 class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
1620     // Generic Motion (mouse hover, joystick...) events go here
1621     @Override
1622     public boolean onGenericMotion(View v, MotionEvent event) {
1623         float x, y;
1624         int action;
1625
1626         switch ( event.getSource() ) {
1627             case InputDevice.SOURCE_JOYSTICK:
1628             case InputDevice.SOURCE_GAMEPAD:
1629             case InputDevice.SOURCE_DPAD:
1630                 SDLActivity.handleJoystickMotionEvent(event);
1631                 return true;
1632
1633             case InputDevice.SOURCE_MOUSE:
1634                 action = event.getActionMasked();
1635                 switch (action) {
1636                     case MotionEvent.ACTION_SCROLL:
1637                         x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
1638                         y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
1639                         SDLActivity.onNativeMouse(0, action, x, y);
1640                         return true;
1641
1642                     case MotionEvent.ACTION_HOVER_MOVE:
1643                         x = event.getX(0);
1644                         y = event.getY(0);
1645
1646                         SDLActivity.onNativeMouse(0, action, x, y);
1647                         return true;
1648
1649                     default:
1650                         break;
1651                 }
1652                 break;
1653
1654             default:
1655                 break;
1656         }
1657
1658         // Event was not managed
1659         return false;
1660     }
1661 }