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