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