1 package org.libsdl.app;
2 
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.Arrays;
6 import java.util.Hashtable;
7 import java.lang.reflect.Method;
8 import java.lang.Math;
9 
10 import android.app.*;
11 import android.content.*;
12 import android.content.res.Configuration;
13 import android.text.InputType;
14 import android.view.*;
15 import android.view.inputmethod.BaseInputConnection;
16 import android.view.inputmethod.EditorInfo;
17 import android.view.inputmethod.InputConnection;
18 import android.view.inputmethod.InputMethodManager;
19 import android.widget.RelativeLayout;
20 import android.widget.Button;
21 import android.widget.LinearLayout;
22 import android.widget.TextView;
23 import android.os.*;
24 import android.preference.PreferenceManager;
25 import android.util.DisplayMetrics;
26 import android.util.Log;
27 import android.util.SparseArray;
28 import android.graphics.*;
29 import android.graphics.drawable.Drawable;
30 import android.hardware.*;
31 import android.content.pm.ActivityInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ApplicationInfo;
34 
35 /**
36     SDL Activity
37 */
38 public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
39     private static final String TAG = "SDL";
40 
41     public static boolean mIsResumedCalled, mHasFocus;
42     public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24);
43     public static boolean mAllowSDLOrientationChanges = false;
44 
45     // Cursor types
46     private static final int SDL_SYSTEM_CURSOR_NONE = -1;
47     private static final int SDL_SYSTEM_CURSOR_ARROW = 0;
48     private static final int SDL_SYSTEM_CURSOR_IBEAM = 1;
49     private static final int SDL_SYSTEM_CURSOR_WAIT = 2;
50     private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3;
51     private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4;
52     private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5;
53     private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6;
54     private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7;
55     private static final int SDL_SYSTEM_CURSOR_SIZENS = 8;
56     private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9;
57     private static final int SDL_SYSTEM_CURSOR_NO = 10;
58     private static final int SDL_SYSTEM_CURSOR_HAND = 11;
59 
60     protected static final int SDL_ORIENTATION_UNKNOWN = 0;
61     protected static final int SDL_ORIENTATION_LANDSCAPE = 1;
62     protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2;
63     protected static final int SDL_ORIENTATION_PORTRAIT = 3;
64     protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4;
65 
66     protected static int mCurrentOrientation;
67 
68     // Handle the state of the native layer
69     public enum NativeState {
70            INIT, RESUMED, PAUSED
71     }
72 
73     public static NativeState mNextNativeState;
74     public static NativeState mCurrentNativeState;
75 
76     /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
77     public static boolean mBrokenLibraries;
78 
79     // Main components
80     protected static SDLActivity mSingleton;
81     protected static SDLSurface mSurface;
82     protected static View mTextEdit;
83     protected static boolean mScreenKeyboardShown;
84     protected static ViewGroup mLayout;
85     protected static SDLClipboardHandler mClipboardHandler;
86     protected static Hashtable<Integer, PointerIcon> mCursors;
87     protected static int mLastCursorID;
88     protected static SDLGenericMotionListener_API12 mMotionListener;
89     protected static HIDDeviceManager mHIDDeviceManager;
90 
91     // This is what SDL runs in. It invokes SDL_main(), eventually
92     protected static Thread mSDLThread;
93 
getMotionListener()94     protected static SDLGenericMotionListener_API12 getMotionListener() {
95         if (mMotionListener == null) {
96             if (Build.VERSION.SDK_INT >= 26) {
97                 mMotionListener = new SDLGenericMotionListener_API26();
98             } else
99             if (Build.VERSION.SDK_INT >= 24) {
100                 mMotionListener = new SDLGenericMotionListener_API24();
101             } else {
102                 mMotionListener = new SDLGenericMotionListener_API12();
103             }
104         }
105 
106         return mMotionListener;
107     }
108 
109     /**
110      * This method returns the name of the shared object with the application entry point
111      * It can be overridden by derived classes.
112      */
getMainSharedObject()113     protected String getMainSharedObject() {
114         String library;
115         String[] libraries = SDLActivity.mSingleton.getLibraries();
116         if (libraries.length > 0) {
117             library = "lib" + libraries[libraries.length - 1] + ".so";
118         } else {
119             library = "libmain.so";
120         }
121         return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
122     }
123 
124     /**
125      * This method returns the name of the application entry point
126      * It can be overridden by derived classes.
127      */
getMainFunction()128     protected String getMainFunction() {
129         return "SDL_main";
130     }
131 
132     /**
133      * This method is called by SDL before loading the native shared libraries.
134      * It can be overridden to provide names of shared libraries to be loaded.
135      * The default implementation returns the defaults. It never returns null.
136      * An array returned by a new implementation must at least contain "SDL2".
137      * Also keep in mind that the order the libraries are loaded may matter.
138      * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
139      */
getLibraries()140     protected String[] getLibraries() {
141         return new String[] {
142             "c++_shared",
143             "hidapi",
144             "SDL2",
145             "SDL2_image",
146             "SDL2_mixer",
147             // "SDL2_net",
148             "SDL2_ttf",
149             "intl-lite",
150             "main"
151         };
152     }
153 
154     // Load the .so
loadLibraries()155     public void loadLibraries() {
156        for (String lib : getLibraries()) {
157           SDL.loadLibrary(lib);
158        }
159     }
160 
161     /**
162      * This method is called by SDL before starting the native application thread.
163      * It can be overridden to provide the arguments after the application name.
164      * The default implementation returns an empty array. It never returns null.
165      * @return arguments for the native application.
166      */
getArguments()167     protected String[] getArguments() {
168         return new String[0];
169     }
170 
initialize()171     public static void initialize() {
172         // The static nature of the singleton and Android quirkyness force us to initialize everything here
173         // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
174         mSingleton = null;
175         mSurface = null;
176         mTextEdit = null;
177         mLayout = null;
178         mClipboardHandler = null;
179         mCursors = new Hashtable<Integer, PointerIcon>();
180         mLastCursorID = 0;
181         mSDLThread = null;
182         mBrokenLibraries = false;
183         mIsResumedCalled = false;
184         mHasFocus = true;
185         mNextNativeState = NativeState.INIT;
186         mCurrentNativeState = NativeState.INIT;
187     }
188 
189     // Setup
190     @Override
onCreate(Bundle savedInstanceState)191     protected void onCreate(Bundle savedInstanceState) {
192         Log.v(TAG, "Device: " + Build.DEVICE);
193         Log.v(TAG, "Model: " + Build.MODEL);
194         Log.v(TAG, "onCreate()");
195         super.onCreate(savedInstanceState);
196 
197         try {
198             Thread.currentThread().setName("SDLActivity");
199         } catch (Exception e) {
200             Log.v(TAG, "modify thread properties failed " + e.toString());
201         }
202 
203         // Load shared libraries
204         String errorMsgBrokenLib = "";
205         try {
206             loadLibraries();
207         } catch(UnsatisfiedLinkError e) {
208             System.err.println(e.getMessage());
209             mBrokenLibraries = true;
210             errorMsgBrokenLib = e.getMessage();
211         } catch(Exception e) {
212             System.err.println(e.getMessage());
213             mBrokenLibraries = true;
214             errorMsgBrokenLib = e.getMessage();
215         }
216 
217         if (mBrokenLibraries)
218         {
219             mSingleton = this;
220             AlertDialog.Builder dlgAlert  = new AlertDialog.Builder(this);
221             dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
222                   + System.getProperty("line.separator")
223                   + System.getProperty("line.separator")
224                   + "Error: " + errorMsgBrokenLib);
225             dlgAlert.setTitle("SDL Error");
226             dlgAlert.setPositiveButton("Exit",
227                 new DialogInterface.OnClickListener() {
228                     @Override
229                     public void onClick(DialogInterface dialog,int id) {
230                         // if this button is clicked, close current activity
231                         SDLActivity.mSingleton.finish();
232                     }
233                 });
234            dlgAlert.setCancelable(false);
235            dlgAlert.create().show();
236 
237            return;
238         }
239 
240         // Set up JNI
241         SDL.setupJNI();
242 
243         // Initialize state
244         SDL.initialize();
245 
246         // So we can call stuff from static callbacks
247         mSingleton = this;
248         SDL.setContext(this);
249 
250         mClipboardHandler = new SDLClipboardHandler_API11();
251 
252         mHIDDeviceManager = HIDDeviceManager.acquire(this);
253 
254         // Set up the surface
255         mSurface = new SDLSurface(getApplication());
256 
257         mLayout = new RelativeLayout(this);
258         mLayout.addView(mSurface);
259         mLayout.setVisibility(View.INVISIBLE); // we will make this visible later through C++ call -> Java
260         setContentView(mLayout);
261 
262         // Get our current screen orientation and pass it down.
263         mCurrentOrientation = SDLActivity.getCurrentOrientation();
264         // Only record current orientation
265         SDLActivity.onNativeOrientationChanged(mCurrentOrientation);
266 
267         setContentView(mLayout);
268 
269         setWindowStyle(false);
270 
271         mLayout.getViewTreeObserver().addOnGlobalLayoutListener(
272             new ViewTreeObserver.OnGlobalLayoutListener() {
273             public void onGlobalLayout(){
274                 Rect r = new Rect();
275                 View view = getWindow().getDecorView();
276                 view.getWindowVisibleDisplayFrame(r);
277                 //Log.v(TAG, "getWindowVisibleDisplayFrame(r): r.left " + r.left + " r.top " + r.top + " r.right " + r.right + " r.bottom " + r.bottom);
278                 SDLActivity.onNativeVisibleDisplayFrameChanged(r.left, r.top, r.right, r.bottom);
279             }
280             });
281 
282         getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
283 
284         // Get filename from "Open with" of another application
285         Intent intent = getIntent();
286         if (intent != null && intent.getData() != null) {
287             String filename = intent.getData().getPath();
288             if (filename != null) {
289                 Log.v(TAG, "Got filename: " + filename);
290                 SDLActivity.onNativeDropFile(filename);
291             }
292         }
293     }
294 
pauseNativeThread()295     protected void pauseNativeThread() {
296         mNextNativeState = NativeState.PAUSED;
297         mIsResumedCalled = false;
298 
299         if (SDLActivity.mBrokenLibraries) {
300             return;
301         }
302 
303         SDLActivity.handleNativeState();
304     }
305 
resumeNativeThread()306     protected void resumeNativeThread() {
307         mNextNativeState = NativeState.RESUMED;
308         mIsResumedCalled = true;
309 
310         if (SDLActivity.mBrokenLibraries) {
311            return;
312         }
313 
314         SDLActivity.handleNativeState();
315     }
316 
317     // Events
318     @Override
onPause()319     protected void onPause() {
320         Log.v(TAG, "onPause()");
321         super.onPause();
322 
323         if (mHIDDeviceManager != null) {
324             mHIDDeviceManager.setFrozen(true);
325         }
326         if (!mHasMultiWindow) {
327             pauseNativeThread();
328         }
329     }
330 
331     @Override
onResume()332     protected void onResume() {
333         Log.v(TAG, "onResume()");
334         super.onResume();
335 
336         if (mHIDDeviceManager != null) {
337             mHIDDeviceManager.setFrozen(false);
338         }
339         if (!mHasMultiWindow) {
340             resumeNativeThread();
341         }
342     }
343 
344     @Override
onStop()345     protected void onStop() {
346         Log.v(TAG, "onStop()");
347         super.onStop();
348         if (mHasMultiWindow) {
349             pauseNativeThread();
350         }
351     }
352 
353     @Override
onStart()354     protected void onStart() {
355         Log.v(TAG, "onStart()");
356         super.onStart();
357         if (mHasMultiWindow) {
358             resumeNativeThread();
359         }
360     }
361 
getCurrentOrientation()362     public static int getCurrentOrientation() {
363         final Context context = SDLActivity.getContext();
364         final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
365 
366         int result = SDL_ORIENTATION_UNKNOWN;
367 
368         switch (display.getRotation()) {
369             case Surface.ROTATION_0:
370                 result = SDL_ORIENTATION_PORTRAIT;
371                 break;
372 
373             case Surface.ROTATION_90:
374                 result = SDL_ORIENTATION_LANDSCAPE;
375                 break;
376 
377             case Surface.ROTATION_180:
378                 result = SDL_ORIENTATION_PORTRAIT_FLIPPED;
379                 break;
380 
381             case Surface.ROTATION_270:
382                 result = SDL_ORIENTATION_LANDSCAPE_FLIPPED;
383                 break;
384         }
385 
386         return result;
387     }
388 
389     @Override
onWindowFocusChanged(boolean hasFocus)390     public void onWindowFocusChanged(boolean hasFocus) {
391         super.onWindowFocusChanged(hasFocus);
392         Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
393 
394         if (SDLActivity.mBrokenLibraries) {
395            return;
396         }
397 
398         mHasFocus = hasFocus;
399         if (hasFocus) {
400            mNextNativeState = NativeState.RESUMED;
401            SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded();
402 
403            SDLActivity.handleNativeState();
404            nativeFocusChanged(true);
405 
406         } else {
407            nativeFocusChanged(false);
408            if (!mHasMultiWindow) {
409                mNextNativeState = NativeState.PAUSED;
410                SDLActivity.handleNativeState();
411            }
412         }
413     }
414 
415     @Override
onLowMemory()416     public void onLowMemory() {
417         Log.v(TAG, "onLowMemory()");
418         super.onLowMemory();
419 
420         if (SDLActivity.mBrokenLibraries) {
421            return;
422         }
423 
424         SDLActivity.nativeLowMemory();
425     }
426 
427     @Override
onDestroy()428     protected void onDestroy() {
429         Log.v(TAG, "onDestroy()");
430 
431         if (mHIDDeviceManager != null) {
432             HIDDeviceManager.release(mHIDDeviceManager);
433             mHIDDeviceManager = null;
434         }
435 
436         if (SDLActivity.mBrokenLibraries) {
437            super.onDestroy();
438            return;
439         }
440 
441         if (SDLActivity.mSDLThread != null) {
442 
443             // Send Quit event to "SDLThread" thread
444             SDLActivity.nativeSendQuit();
445 
446             // Wait for "SDLThread" thread to end
447             try {
448                 SDLActivity.mSDLThread.join();
449             } catch(Exception e) {
450                 Log.v(TAG, "Problem stopping SDLThread: " + e);
451             }
452         }
453 
454         SDLActivity.nativeQuit();
455 
456         super.onDestroy();
457     }
458 
459     @Override
onBackPressed()460     public void onBackPressed() {
461         // Check if we want to block the back button in case of mouse right click.
462         //
463         // If we do, the normal hardware back button will no longer work and people have to use home,
464         // but the mouse right click will work.
465         //
466         String trapBack = SDLActivity.nativeGetHint("SDL_ANDROID_TRAP_BACK_BUTTON");
467         if ((trapBack != null) && trapBack.equals("1")) {
468             // Exit and let the mouse handler handle this button (if appropriate)
469             return;
470         }
471 
472         // Default system back button behavior.
473         if (!isFinishing()) {
474             super.onBackPressed();
475         }
476     }
477 
478     // Called by JNI from SDL.
manualBackButton()479     public static void manualBackButton() {
480         mSingleton.pressBackButton();
481     }
482 
483     // Used to get us onto the activity's main thread
pressBackButton()484     public void pressBackButton() {
485         runOnUiThread(new Runnable() {
486             @Override
487             public void run() {
488                 if (!SDLActivity.this.isFinishing()) {
489                     SDLActivity.this.superOnBackPressed();
490                 }
491             }
492         });
493     }
494 
495     // Used to access the system back behavior.
superOnBackPressed()496     public void superOnBackPressed() {
497         super.onBackPressed();
498     }
499 
500     @Override
dispatchKeyEvent(KeyEvent event)501     public boolean dispatchKeyEvent(KeyEvent event) {
502 
503         if (SDLActivity.mBrokenLibraries) {
504            return false;
505         }
506 
507         int keyCode = event.getKeyCode();
508         // Ignore certain special keys so they're handled by Android
509         if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
510             keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
511             keyCode == KeyEvent.KEYCODE_CAMERA ||
512             keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */
513             keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */
514             ) {
515             return false;
516         }
517         return super.dispatchKeyEvent(event);
518     }
519 
520     /* Transition to next state */
handleNativeState()521     public static void handleNativeState() {
522 
523         if (mNextNativeState == mCurrentNativeState) {
524             // Already in same state, discard.
525             return;
526         }
527 
528         // Try a transition to init state
529         if (mNextNativeState == NativeState.INIT) {
530 
531             mCurrentNativeState = mNextNativeState;
532             return;
533         }
534 
535         // Try a transition to paused state
536         if (mNextNativeState == NativeState.PAUSED) {
537             if (mSDLThread != null) {
538                 nativePause();
539             }
540             if (mSurface != null) {
541                 mSurface.handlePause();
542             }
543             mCurrentNativeState = mNextNativeState;
544             return;
545         }
546 
547         // Try a transition to resumed state
548         if (mNextNativeState == NativeState.RESUMED) {
549             if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) {
550                 if (mSDLThread == null) {
551                     // This is the entry point to the C app.
552                     // Start up the C app thread and enable sensor input for the first time
553                     // FIXME: Why aren't we enabling sensor input at start?
554 
555                     mSDLThread = new Thread(new SDLMain(), "SDLThread");
556                     mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true);
557                     mSDLThread.start();
558 
559                     // No nativeResume(), don't signal Android_ResumeSem
560                     mSurface.handleResume();
561                 } else {
562                     nativeResume();
563                     mSurface.handleResume();
564                 }
565 
566                 mCurrentNativeState = mNextNativeState;
567             }
568         }
569     }
570 
571     // Messages from the SDLMain thread
572     static final int COMMAND_CHANGE_TITLE = 1;
573     static final int COMMAND_CHANGE_WINDOW_STYLE = 2;
574     static final int COMMAND_TEXTEDIT_HIDE = 3;
575     static final int COMMAND_CHANGE_SURFACEVIEW_FORMAT = 4;
576     static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
577 
578     protected static final int COMMAND_USER = 0x8000;
579 
580     protected static boolean mFullscreenModeActive;
581 
582     /**
583      * This method is called by SDL if SDL did not handle a message itself.
584      * This happens if a received message contains an unsupported command.
585      * Method can be overwritten to handle Messages in a different class.
586      * @param command the command of the message.
587      * @param param the parameter of the message. May be null.
588      * @return if the message was handled in overridden method.
589      */
onUnhandledMessage(int command, Object param)590     protected boolean onUnhandledMessage(int command, Object param) {
591         return false;
592     }
593 
594     /**
595      * A Handler class for Messages from native SDL applications.
596      * It uses current Activities as target (e.g. for the title).
597      * static to prevent implicit references to enclosing object.
598      */
599     protected static class SDLCommandHandler extends Handler {
600         @Override
handleMessage(Message msg)601         public void handleMessage(Message msg) {
602             Context context = SDL.getContext();
603             if (context == null) {
604                 Log.e(TAG, "error handling message, getContext() returned null");
605                 return;
606             }
607             switch (msg.arg1) {
608             case COMMAND_CHANGE_TITLE:
609                 if (context instanceof Activity) {
610                     ((Activity) context).setTitle((String)msg.obj);
611                 } else {
612                     Log.e(TAG, "error handling message, getContext() returned no Activity");
613                 }
614                 break;
615             case COMMAND_CHANGE_WINDOW_STYLE:
616                 if (Build.VERSION.SDK_INT < 19) {
617                     // This version of Android doesn't support the immersive fullscreen mode
618                     break;
619                 }
620                 if (context instanceof Activity) {
621                     Window window = ((Activity) context).getWindow();
622                     if (window != null) {
623                         if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
624                             int flags = View.SYSTEM_UI_FLAG_FULLSCREEN |
625                                         View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
626                                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
627                                         View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
628                                         View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
629                                         View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE;
630                             window.getDecorView().setSystemUiVisibility(flags);
631                             window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
632                             window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
633                             SDLActivity.mFullscreenModeActive = true;
634                         } else {
635                             int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE;
636                             window.getDecorView().setSystemUiVisibility(flags);
637                             Context appContext = context.getApplicationContext();
638                             boolean forceFullScreen = PreferenceManager.getDefaultSharedPreferences(appContext).getBoolean("Force fullscreen", false);
639                             window.addFlags(forceFullScreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
640                             window.clearFlags(forceFullScreen ? WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN : WindowManager.LayoutParams.FLAG_FULLSCREEN);
641                             SDLActivity.mFullscreenModeActive = forceFullScreen;
642                         }
643                     }
644                 } else {
645                     Log.e(TAG, "error handling message, getContext() returned no Activity");
646                 }
647                 break;
648             case COMMAND_TEXTEDIT_HIDE:
649                 if (mTextEdit != null) {
650                     // Note: On some devices setting view to GONE creates a flicker in landscape.
651                     // Setting the View's sizes to 0 is similar to GONE but without the flicker.
652                     // The sizes will be set to useful values when the keyboard is shown again.
653                     mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0));
654 
655                     InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
656                     imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
657 
658                     mScreenKeyboardShown = false;
659 
660                     mSurface.requestFocus();
661                 }
662                 break;
663             case COMMAND_SET_KEEP_SCREEN_ON:
664             {
665                 if (context instanceof Activity) {
666                     Window window = ((Activity) context).getWindow();
667                     if (window != null) {
668                         if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
669                             window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
670                         } else {
671                             window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
672                         }
673                     }
674                 }
675                 break;
676             }
677             case COMMAND_CHANGE_SURFACEVIEW_FORMAT:
678             {
679                 int format = (Integer) msg.obj;
680                 int pf;
681 
682                 if (SDLActivity.mSurface == null) {
683                     return;
684                 }
685 
686                 SurfaceHolder holder = SDLActivity.mSurface.getHolder();
687                 if (holder == null) {
688                     return;
689                 }
690 
691                 if (format == 1) {
692                     pf = PixelFormat.RGBA_8888;
693                 } else if (format == 2) {
694                     pf = PixelFormat.RGBX_8888;
695                 } else {
696                     pf = PixelFormat.RGB_565;
697                 }
698 
699                 holder.setFormat(pf);
700 
701                 break;
702             }
703             default:
704                 if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
705                     Log.e(TAG, "error handling message, command is " + msg.arg1);
706                 }
707             }
708         }
709     }
710 
711     // Handler for the messages
712     Handler commandHandler = new SDLCommandHandler();
713 
714     // Send a message from the SDLMain thread
sendCommand(int command, Object data)715     boolean sendCommand(int command, Object data) {
716         Message msg = commandHandler.obtainMessage();
717         msg.arg1 = command;
718         msg.obj = data;
719         boolean result = commandHandler.sendMessage(msg);
720 
721         if ((Build.VERSION.SDK_INT >= 19) && (command == COMMAND_CHANGE_WINDOW_STYLE)) {
722             // Ensure we don't return until the resize has actually happened,
723             // or 500ms have passed.
724 
725             boolean bShouldWait = false;
726 
727             if (data instanceof Integer) {
728                 // Let's figure out if we're already laid out fullscreen or not.
729                 Display display = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
730                 android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics();
731                 display.getRealMetrics( realMetrics );
732 
733                 boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) &&
734                                              (realMetrics.heightPixels == mSurface.getHeight()));
735 
736                 if (((Integer)data).intValue() == 1) {
737                     // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going
738                     // to change size and should wait for surfaceChanged() before we return, so the size
739                     // is right back in native code.  If we're already laid out fullscreen, though, we're
740                     // not going to change size even if we change decor modes, so we shouldn't wait for
741                     // surfaceChanged() -- which may not even happen -- and should return immediately.
742                     bShouldWait = !bFullscreenLayout;
743                 }
744                 else {
745                     // If we're laid out fullscreen (even if the status bar and nav bar are present),
746                     // or are actively in fullscreen, we're going to change size and should wait for
747                     // surfaceChanged before we return, so the size is right back in native code.
748                     bShouldWait = bFullscreenLayout;
749                 }
750             }
751 
752             if (bShouldWait && (SDLActivity.getContext() != null)) {
753                 // We'll wait for the surfaceChanged() method, which will notify us
754                 // when called.  That way, we know our current size is really the
755                 // size we need, instead of grabbing a size that's still got
756                 // the navigation and/or status bars before they're hidden.
757                 //
758                 // We'll wait for up to half a second, because some devices
759                 // take a surprisingly long time for the surface resize, but
760                 // then we'll just give up and return.
761                 //
762                 synchronized(SDLActivity.getContext()) {
763                     try {
764                         SDLActivity.getContext().wait(500);
765                     }
766                     catch (InterruptedException ie) {
767                         ie.printStackTrace();
768                     }
769                 }
770             }
771         }
772 
773         return result;
774     }
775 
776     // C functions we call
nativeSetupJNI()777     public static native int nativeSetupJNI();
nativeRunMain(String library, String function, Object arguments)778     public static native int nativeRunMain(String library, String function, Object arguments);
nativeLowMemory()779     public static native void nativeLowMemory();
nativeSendQuit()780     public static native void nativeSendQuit();
nativeQuit()781     public static native void nativeQuit();
nativePause()782     public static native void nativePause();
nativeResume()783     public static native void nativeResume();
nativeFocusChanged(boolean hasFocus)784     public static native void nativeFocusChanged(boolean hasFocus);
onNativeDropFile(String filename)785     public static native void onNativeDropFile(String filename);
onNativeVisibleDisplayFrameChanged(int left, int top, int right, int bottom)786     public static native void onNativeVisibleDisplayFrameChanged(int left, int top, int right, int bottom);
nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, int format, float rate)787     public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, int format, float rate);
onNativeResize()788     public static native void onNativeResize();
onNativeKeyDown(int keycode)789     public static native void onNativeKeyDown(int keycode);
onNativeKeyUp(int keycode)790     public static native void onNativeKeyUp(int keycode);
onNativeSoftReturnKey()791     public static native boolean onNativeSoftReturnKey();
onNativeKeyboardFocusLost()792     public static native void onNativeKeyboardFocusLost();
onNativeMouse(int button, int action, float x, float y, boolean relative)793     public static native void onNativeMouse(int button, int action, float x, float y, boolean relative);
onNativeTouch(int touchDevId, int pointerFingerId, int action, float x, float y, float p)794     public static native void onNativeTouch(int touchDevId, int pointerFingerId,
795                                             int action, float x,
796                                             float y, float p);
onNativeAccel(float x, float y, float z)797     public static native void onNativeAccel(float x, float y, float z);
onNativeClipboardChanged()798     public static native void onNativeClipboardChanged();
onNativeSurfaceCreated()799     public static native void onNativeSurfaceCreated();
onNativeSurfaceChanged()800     public static native void onNativeSurfaceChanged();
onNativeSurfaceDestroyed()801     public static native void onNativeSurfaceDestroyed();
nativeGetHint(String name)802     public static native String nativeGetHint(String name);
nativeSetenv(String name, String value)803     public static native void nativeSetenv(String name, String value);
onNativeOrientationChanged(int orientation)804     public static native void onNativeOrientationChanged(int orientation);
nativeAddTouch(int touchId, String name)805     public static native void nativeAddTouch(int touchId, String name);
nativePermissionResult(int requestCode, boolean result)806     public static native void nativePermissionResult(int requestCode, boolean result);
807 
808     /**
809      * This method is called by SDL using JNI.
810      */
setActivityTitle(String title)811     public static boolean setActivityTitle(String title) {
812         // Called from SDLMain() thread and can't directly affect the view
813         return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
814     }
815 
816     /**
817      * This method is called by SDL using JNI.
818      */
setWindowStyle(boolean fullscreen)819     public static void setWindowStyle(boolean fullscreen) {
820         // Called from SDLMain() thread and can't directly affect the view
821         mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0);
822     }
823 
824     /**
825      * This method is called by SDL using JNI.
826      * This is a static method for JNI convenience, it calls a non-static method
827      * so that is can be overridden
828      */
setOrientation(int w, int h, boolean resizable, String hint)829     public static void setOrientation(int w, int h, boolean resizable, String hint)
830     {
831         if (mSingleton != null) {
832             mSingleton.setOrientationBis(w, h, resizable, hint);
833         }
834     }
835 
836     /**
837      * This can be overridden
838      */
setOrientationBis(int w, int h, boolean resizable, String hint)839     public void setOrientationBis(int w, int h, boolean resizable, String hint)
840     {
841         int orientation_landscape = -1;
842         int orientation_portrait = -1;
843 
844         /* If set, hint "explicitly controls which UI orientations are allowed". */
845         if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) {
846             orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
847         } else if (hint.contains("LandscapeRight")) {
848             orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
849         } else if (hint.contains("LandscapeLeft")) {
850             orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
851         }
852 
853         if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) {
854             orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
855         } else if (hint.contains("Portrait")) {
856             orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
857         } else if (hint.contains("PortraitUpsideDown")) {
858             orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
859         }
860 
861         boolean is_landscape_allowed = (orientation_landscape == -1 ? false : true);
862         boolean is_portrait_allowed = (orientation_portrait == -1 ? false : true);
863         int req = -1; /* Requested orientation */
864 
865         /* No valid hint, nothing is explicitly allowed */
866         if (!is_portrait_allowed && !is_landscape_allowed) {
867             if (resizable) {
868                 /* All orientations are allowed */
869                 req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
870             } else {
871                 /* Fixed window and nothing specified. Get orientation from w/h of created window */
872                 req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
873             }
874         } else {
875             /* At least one orientation is allowed */
876             if (resizable) {
877                 if (is_portrait_allowed && is_landscape_allowed) {
878                     /* hint allows both landscape and portrait, promote to full sensor */
879                     req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
880                 } else {
881                     /* Use the only one allowed "orientation" */
882                     req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
883                 }
884             } else {
885                 /* Fixed window and both orientations are allowed. Choose one. */
886                 if (is_portrait_allowed && is_landscape_allowed) {
887                     req = (w > h ? orientation_landscape : orientation_portrait);
888                 } else {
889                     /* Use the only one allowed "orientation" */
890                     req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
891                 }
892             }
893         }
894 
895         Log.v("SDL", "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint);
896         if (mAllowSDLOrientationChanges && req != -1) {
897             mSingleton.setRequestedOrientation(req);
898         }
899     }
900 
901     /**
902      * This method is called by SDL using JNI.
903      */
minimizeWindow()904     public static void minimizeWindow() {
905 
906         if (mSingleton == null) {
907             return;
908         }
909 
910         Intent startMain = new Intent(Intent.ACTION_MAIN);
911         startMain.addCategory(Intent.CATEGORY_HOME);
912         startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
913         mSingleton.startActivity(startMain);
914     }
915 
916     /**
917      * This method is called by SDL using JNI.
918      */
shouldMinimizeOnFocusLoss()919     public static boolean shouldMinimizeOnFocusLoss() {
920 /*
921         if (Build.VERSION.SDK_INT >= 24) {
922             if (mSingleton == null) {
923                 return true;
924             }
925 
926             if (mSingleton.isInMultiWindowMode()) {
927                 return false;
928             }
929 
930             if (mSingleton.isInPictureInPictureMode()) {
931                 return false;
932             }
933         }
934 
935         return true;
936 */
937         return false;
938     }
939 
940     /**
941      * This method is called by SDL using JNI.
942      */
isScreenKeyboardShown()943     public static boolean isScreenKeyboardShown()
944     {
945         if (mTextEdit == null) {
946             return false;
947         }
948 
949         if (!mScreenKeyboardShown) {
950             return false;
951         }
952 
953         InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
954         return imm.isAcceptingText();
955 
956     }
957 
958     /**
959      * This method is called by SDL using JNI.
960      */
supportsRelativeMouse()961     public static boolean supportsRelativeMouse()
962     {
963         // ChromeOS doesn't provide relative mouse motion via the Android 7 APIs
964         if (isChromebook()) {
965             return false;
966         }
967 
968         // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under
969         // Android 7 APIs, and simply returns no data under Android 8 APIs.
970         //
971         // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and
972         // thus SDK version 27.  If we are in DeX mode and not API 27 or higher, as a result,
973         // we should stick to relative mode.
974         //
975         if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) {
976             return false;
977         }
978 
979         return SDLActivity.getMotionListener().supportsRelativeMouse();
980     }
981 
982     /**
983      * This method is called by SDL using JNI.
984      */
setRelativeMouseEnabled(boolean enabled)985     public static boolean setRelativeMouseEnabled(boolean enabled)
986     {
987         if (enabled && !supportsRelativeMouse()) {
988             return false;
989         }
990 
991         return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled);
992     }
993 
994     /**
995      * This method is called by SDL using JNI.
996      */
sendMessage(int command, int param)997     public static boolean sendMessage(int command, int param) {
998         if (mSingleton == null) {
999             return false;
1000         }
1001         return mSingleton.sendCommand(command, Integer.valueOf(param));
1002     }
1003 
1004     /**
1005      * This method is called by SDL using JNI.
1006      */
getContext()1007     public static Context getContext() {
1008         return SDL.getContext();
1009     }
1010 
1011     /**
1012      * This method is called by SDL using JNI.
1013      */
isAndroidTV()1014     public static boolean isAndroidTV() {
1015         UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE);
1016         if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
1017             return true;
1018         }
1019         if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) {
1020             return true;
1021         }
1022         if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) {
1023             return true;
1024         }
1025         if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV")) {
1026             return true;
1027         }
1028         return false;
1029     }
1030 
1031     /**
1032      * This method is called by SDL using JNI.
1033      */
isTablet()1034     public static boolean isTablet() {
1035         DisplayMetrics metrics = new DisplayMetrics();
1036         Activity activity = (Activity)getContext();
1037         if (activity == null) {
1038             return false;
1039         }
1040         activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
1041 
1042         double dWidthInches = metrics.widthPixels / (double)metrics.xdpi;
1043         double dHeightInches = metrics.heightPixels / (double)metrics.ydpi;
1044 
1045         double dDiagonal = Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches));
1046 
1047         // If our diagonal size is seven inches or greater, we consider ourselves a tablet.
1048         return (dDiagonal >= 7.0);
1049     }
1050 
1051     /**
1052      * This method is called by SDL using JNI.
1053      */
isChromebook()1054     public static boolean isChromebook() {
1055         if (getContext() == null) {
1056             return false;
1057         }
1058         return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
1059     }
1060 
1061     /**
1062      * This method is called by SDL using JNI.
1063      */
isDeXMode()1064     public static boolean isDeXMode() {
1065         if (Build.VERSION.SDK_INT < 24) {
1066             return false;
1067         }
1068         try {
1069             final Configuration config = getContext().getResources().getConfiguration();
1070             final Class configClass = config.getClass();
1071             return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
1072                     == configClass.getField("semDesktopModeEnabled").getInt(config);
1073         } catch(Exception ignored) {
1074             return false;
1075         }
1076     }
1077 
1078     /**
1079      * This method is called by SDL using JNI.
1080      */
getDisplayDPI()1081     public static DisplayMetrics getDisplayDPI() {
1082         return getContext().getResources().getDisplayMetrics();
1083     }
1084 
1085     /**
1086      * This method is called by SDL using JNI.
1087      */
getManifestEnvironmentVariables()1088     public static boolean getManifestEnvironmentVariables() {
1089         try {
1090             ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA);
1091             Bundle bundle = applicationInfo.metaData;
1092             if (bundle == null) {
1093                 return false;
1094             }
1095             String prefix = "SDL_ENV.";
1096             final int trimLength = prefix.length();
1097             for (String key : bundle.keySet()) {
1098                 if (key.startsWith(prefix)) {
1099                     String name = key.substring(trimLength);
1100                     String value = bundle.get(key).toString();
1101                     nativeSetenv(name, value);
1102                 }
1103             }
1104             /* environment variables set! */
1105             return true;
1106         } catch (Exception e) {
1107            Log.v("SDL", "exception " + e.toString());
1108         }
1109         return false;
1110     }
1111 
1112     // This method is called by SDLControllerManager's API 26 Generic Motion Handler.
getContentView()1113     public static View getContentView()
1114     {
1115         return mSingleton.mLayout;
1116     }
1117 
1118     static class ShowTextInputTask implements Runnable {
1119         /*
1120          * This is used to regulate the pan&scan method to have some offset from
1121          * the bottom edge of the input region and the top edge of an input
1122          * method (soft keyboard)
1123          */
1124         static final int HEIGHT_PADDING = 15;
1125 
1126         public int x, y, w, h;
1127 
ShowTextInputTask(int x, int y, int w, int h)1128         public ShowTextInputTask(int x, int y, int w, int h) {
1129             this.x = x;
1130             this.y = y;
1131             this.w = w;
1132             this.h = h;
1133 
1134             /* Minimum size of 1 pixel, so it takes focus. */
1135             if (this.w <= 0) {
1136                 this.w = 1;
1137             }
1138             if (this.h + HEIGHT_PADDING <= 0) {
1139                 this.h = 1 - HEIGHT_PADDING;
1140             }
1141         }
1142 
1143         @Override
run()1144         public void run() {
1145             RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING);
1146             params.leftMargin = x;
1147             params.topMargin = y;
1148 
1149             if (mTextEdit == null) {
1150                 mTextEdit = new DummyEdit(SDL.getContext());
1151 
1152                 mLayout.addView(mTextEdit, params);
1153             } else {
1154                 mTextEdit.setLayoutParams(params);
1155             }
1156 
1157             mTextEdit.setVisibility(View.VISIBLE);
1158             mTextEdit.requestFocus();
1159 
1160             InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1161             imm.showSoftInput(mTextEdit, 0);
1162 
1163             mScreenKeyboardShown = true;
1164         }
1165     }
1166 
1167     /**
1168      * This method is called by SDL using JNI.
1169      */
showTextInput(int x, int y, int w, int h)1170     public static boolean showTextInput(int x, int y, int w, int h) {
1171         // Transfer the task to the main thread as a Runnable
1172         return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h));
1173     }
1174 
isTextInputEvent(KeyEvent event)1175     public static boolean isTextInputEvent(KeyEvent event) {
1176 
1177         // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT
1178         if (event.isCtrlPressed()) {
1179             return false;
1180         }
1181 
1182         return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE;
1183     }
1184 
1185     /**
1186      * This method is called by SDL using JNI.
1187      */
getNativeSurface()1188     public static Surface getNativeSurface() {
1189         if (SDLActivity.mSurface == null) {
1190             return null;
1191         }
1192         return SDLActivity.mSurface.getNativeSurface();
1193     }
1194 
1195     /**
1196      * This method is called by SDL using JNI.
1197      */
setSurfaceViewFormat(int format)1198     public static void setSurfaceViewFormat(int format) {
1199         mSingleton.sendCommand(COMMAND_CHANGE_SURFACEVIEW_FORMAT, format);
1200         return;
1201     }
1202 
1203     // Input
1204 
1205     /**
1206      * This method is called by SDL using JNI.
1207      */
initTouch()1208     public static void initTouch() {
1209         int[] ids = InputDevice.getDeviceIds();
1210 
1211         for (int i = 0; i < ids.length; ++i) {
1212             InputDevice device = InputDevice.getDevice(ids[i]);
1213             if (device != null && (device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) != 0) {
1214                 nativeAddTouch(device.getId(), device.getName());
1215             }
1216         }
1217     }
1218 
1219     // APK expansion files support
1220 
1221     /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */
1222     private static Object expansionFile;
1223 
1224     /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */
1225     private static Method expansionFileMethod;
1226 
1227     /**
1228      * This method is called by SDL using JNI.
1229      * @return an InputStream on success or null if no expansion file was used.
1230      * @throws IOException on errors. Message is set for the SDL error message.
1231      */
openAPKExpansionInputStream(String fileName)1232     public static InputStream openAPKExpansionInputStream(String fileName) throws IOException {
1233         // Get a ZipResourceFile representing a merger of both the main and patch files
1234         if (expansionFile == null) {
1235             String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION");
1236             if (mainHint == null) {
1237                 return null; // no expansion use if no main version was set
1238             }
1239             String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION");
1240             if (patchHint == null) {
1241                 return null; // no expansion use if no patch version was set
1242             }
1243 
1244             Integer mainVersion;
1245             Integer patchVersion;
1246             try {
1247                 mainVersion = Integer.valueOf(mainHint);
1248                 patchVersion = Integer.valueOf(patchHint);
1249             } catch (NumberFormatException ex) {
1250                 ex.printStackTrace();
1251                 throw new IOException("No valid file versions set for APK expansion files", ex);
1252             }
1253 
1254             try {
1255                 // To avoid direct dependency on Google APK expansion library that is
1256                 // not a part of Android SDK we access it using reflection
1257                 expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport")
1258                     .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class)
1259                     .invoke(null, SDL.getContext(), mainVersion, patchVersion);
1260 
1261                 expansionFileMethod = expansionFile.getClass()
1262                     .getMethod("getInputStream", String.class);
1263             } catch (Exception ex) {
1264                 ex.printStackTrace();
1265                 expansionFile = null;
1266                 expansionFileMethod = null;
1267                 throw new IOException("Could not access APK expansion support library", ex);
1268             }
1269         }
1270 
1271         // Get an input stream for a known file inside the expansion file ZIPs
1272         InputStream fileStream;
1273         try {
1274             fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName);
1275         } catch (Exception ex) {
1276             // calling "getInputStream" failed
1277             ex.printStackTrace();
1278             throw new IOException("Could not open stream from APK expansion file", ex);
1279         }
1280 
1281         if (fileStream == null) {
1282             // calling "getInputStream" was successful but null was returned
1283             throw new IOException("Could not find path in APK expansion file");
1284         }
1285 
1286         return fileStream;
1287     }
1288 
1289     // Messagebox
1290 
1291     /** Result of current messagebox. Also used for blocking the calling thread. */
1292     protected final int[] messageboxSelection = new int[1];
1293 
1294     /** Id of current dialog. */
1295     protected int dialogs = 0;
1296 
1297     /**
1298      * This method is called by SDL using JNI.
1299      * Shows the messagebox from UI thread and block calling thread.
1300      * buttonFlags, buttonIds and buttonTexts must have same length.
1301      * @param buttonFlags array containing flags for every button.
1302      * @param buttonIds array containing id for every button.
1303      * @param buttonTexts array containing text for every button.
1304      * @param colors null for default or array of length 5 containing colors.
1305      * @return button id or -1.
1306      */
messageboxShowMessageBox( final int flags, final String title, final String message, final int[] buttonFlags, final int[] buttonIds, final String[] buttonTexts, final int[] colors)1307     public int messageboxShowMessageBox(
1308             final int flags,
1309             final String title,
1310             final String message,
1311             final int[] buttonFlags,
1312             final int[] buttonIds,
1313             final String[] buttonTexts,
1314             final int[] colors) {
1315 
1316         messageboxSelection[0] = -1;
1317 
1318         // sanity checks
1319 
1320         if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
1321             return -1; // implementation broken
1322         }
1323 
1324         // collect arguments for Dialog
1325 
1326         final Bundle args = new Bundle();
1327         args.putInt("flags", flags);
1328         args.putString("title", title);
1329         args.putString("message", message);
1330         args.putIntArray("buttonFlags", buttonFlags);
1331         args.putIntArray("buttonIds", buttonIds);
1332         args.putStringArray("buttonTexts", buttonTexts);
1333         args.putIntArray("colors", colors);
1334 
1335         // trigger Dialog creation on UI thread
1336 
1337         runOnUiThread(new Runnable() {
1338             @Override
1339             public void run() {
1340                 showDialog(dialogs++, args);
1341             }
1342         });
1343 
1344         // block the calling thread
1345 
1346         synchronized (messageboxSelection) {
1347             try {
1348                 messageboxSelection.wait();
1349             } catch (InterruptedException ex) {
1350                 ex.printStackTrace();
1351                 return -1;
1352             }
1353         }
1354 
1355         // return selected value
1356 
1357         return messageboxSelection[0];
1358     }
1359 
1360     @Override
onCreateDialog(int ignore, Bundle args)1361     protected Dialog onCreateDialog(int ignore, Bundle args) {
1362 
1363         // TODO set values from "flags" to messagebox dialog
1364 
1365         // get colors
1366 
1367         int[] colors = args.getIntArray("colors");
1368         int backgroundColor;
1369         int textColor;
1370         int buttonBorderColor;
1371         int buttonBackgroundColor;
1372         int buttonSelectedColor;
1373         if (colors != null) {
1374             int i = -1;
1375             backgroundColor = colors[++i];
1376             textColor = colors[++i];
1377             buttonBorderColor = colors[++i];
1378             buttonBackgroundColor = colors[++i];
1379             buttonSelectedColor = colors[++i];
1380         } else {
1381             backgroundColor = Color.TRANSPARENT;
1382             textColor = Color.TRANSPARENT;
1383             buttonBorderColor = Color.TRANSPARENT;
1384             buttonBackgroundColor = Color.TRANSPARENT;
1385             buttonSelectedColor = Color.TRANSPARENT;
1386         }
1387 
1388         // create dialog with title and a listener to wake up calling thread
1389 
1390         final Dialog dialog = new Dialog(this);
1391         dialog.setTitle(args.getString("title"));
1392         dialog.setCancelable(false);
1393         dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
1394             @Override
1395             public void onDismiss(DialogInterface unused) {
1396                 synchronized (messageboxSelection) {
1397                     messageboxSelection.notify();
1398                 }
1399             }
1400         });
1401 
1402         // create text
1403 
1404         TextView message = new TextView(this);
1405         message.setGravity(Gravity.CENTER);
1406         message.setText(args.getString("message"));
1407         if (textColor != Color.TRANSPARENT) {
1408             message.setTextColor(textColor);
1409         }
1410 
1411         // create buttons
1412 
1413         int[] buttonFlags = args.getIntArray("buttonFlags");
1414         int[] buttonIds = args.getIntArray("buttonIds");
1415         String[] buttonTexts = args.getStringArray("buttonTexts");
1416 
1417         final SparseArray<Button> mapping = new SparseArray<Button>();
1418 
1419         LinearLayout buttons = new LinearLayout(this);
1420         buttons.setOrientation(LinearLayout.HORIZONTAL);
1421         buttons.setGravity(Gravity.CENTER);
1422         for (int i = 0; i < buttonTexts.length; ++i) {
1423             Button button = new Button(this);
1424             final int id = buttonIds[i];
1425             button.setOnClickListener(new View.OnClickListener() {
1426                 @Override
1427                 public void onClick(View v) {
1428                     messageboxSelection[0] = id;
1429                     dialog.dismiss();
1430                 }
1431             });
1432             if (buttonFlags[i] != 0) {
1433                 // see SDL_messagebox.h
1434                 if ((buttonFlags[i] & 0x00000001) != 0) {
1435                     mapping.put(KeyEvent.KEYCODE_ENTER, button);
1436                 }
1437                 if ((buttonFlags[i] & 0x00000002) != 0) {
1438                     mapping.put(KeyEvent.KEYCODE_ESCAPE, button); /* API 11 */
1439                 }
1440             }
1441             button.setText(buttonTexts[i]);
1442             if (textColor != Color.TRANSPARENT) {
1443                 button.setTextColor(textColor);
1444             }
1445             if (buttonBorderColor != Color.TRANSPARENT) {
1446                 // TODO set color for border of messagebox button
1447             }
1448             if (buttonBackgroundColor != Color.TRANSPARENT) {
1449                 Drawable drawable = button.getBackground();
1450                 if (drawable == null) {
1451                     // setting the color this way removes the style
1452                     button.setBackgroundColor(buttonBackgroundColor);
1453                 } else {
1454                     // setting the color this way keeps the style (gradient, padding, etc.)
1455                     drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
1456                 }
1457             }
1458             if (buttonSelectedColor != Color.TRANSPARENT) {
1459                 // TODO set color for selected messagebox button
1460             }
1461             buttons.addView(button);
1462         }
1463 
1464         // create content
1465 
1466         LinearLayout content = new LinearLayout(this);
1467         content.setOrientation(LinearLayout.VERTICAL);
1468         content.addView(message);
1469         content.addView(buttons);
1470         if (backgroundColor != Color.TRANSPARENT) {
1471             content.setBackgroundColor(backgroundColor);
1472         }
1473 
1474         // add content to dialog and return
1475 
1476         dialog.setContentView(content);
1477         dialog.setOnKeyListener(new Dialog.OnKeyListener() {
1478             @Override
1479             public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) {
1480                 Button button = mapping.get(keyCode);
1481                 if (button != null) {
1482                     if (event.getAction() == KeyEvent.ACTION_UP) {
1483                         button.performClick();
1484                     }
1485                     return true; // also for ignored actions
1486                 }
1487                 return false;
1488             }
1489         });
1490 
1491         return dialog;
1492     }
1493 
1494     private final Runnable rehideSystemUi = new Runnable() {
1495         @Override
1496         public void run() {
1497             int flags = View.SYSTEM_UI_FLAG_FULLSCREEN |
1498                         View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1499                         View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
1500                         View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
1501                         View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
1502                         View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE;
1503 
1504             SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags);
1505         }
1506     };
1507 
onSystemUiVisibilityChange(int visibility)1508     public void onSystemUiVisibilityChange(int visibility) {
1509         if (SDLActivity.mFullscreenModeActive && ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)) {
1510 
1511             Handler handler = getWindow().getDecorView().getHandler();
1512             if (handler != null) {
1513                 handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop.
1514                 handler.postDelayed(rehideSystemUi, 2000);
1515             }
1516 
1517         }
1518     }
1519 
1520     /**
1521      * This method is called by SDL using JNI.
1522      */
clipboardHasText()1523     public static boolean clipboardHasText() {
1524         return mClipboardHandler.clipboardHasText();
1525     }
1526 
1527     /**
1528      * This method is called by SDL using JNI.
1529      */
clipboardGetText()1530     public static String clipboardGetText() {
1531         return mClipboardHandler.clipboardGetText();
1532     }
1533 
1534     /**
1535      * This method is called by SDL using JNI.
1536      */
clipboardSetText(String string)1537     public static void clipboardSetText(String string) {
1538         mClipboardHandler.clipboardSetText(string);
1539     }
1540 
1541     /**
1542      * This method is called by SDL using JNI.
1543      */
createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY)1544     public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) {
1545         Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
1546         ++mLastCursorID;
1547 
1548         if (Build.VERSION.SDK_INT >= 24) {
1549             try {
1550                 mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY));
1551             } catch (Exception e) {
1552                 return 0;
1553             }
1554         } else {
1555             return 0;
1556         }
1557         return mLastCursorID;
1558     }
1559 
1560     /**
1561      * This method is called by SDL using JNI.
1562      */
setCustomCursor(int cursorID)1563     public static boolean setCustomCursor(int cursorID) {
1564 
1565         if (Build.VERSION.SDK_INT >= 24) {
1566             try {
1567                 mSurface.setPointerIcon(mCursors.get(cursorID));
1568             } catch (Exception e) {
1569                 return false;
1570             }
1571         } else {
1572             return false;
1573         }
1574         return true;
1575     }
1576 
1577     /**
1578      * This method is called by SDL using JNI.
1579      */
setSystemCursor(int cursorID)1580     public static boolean setSystemCursor(int cursorID) {
1581         int cursor_type = 0; //PointerIcon.TYPE_NULL;
1582         switch (cursorID) {
1583         case SDL_SYSTEM_CURSOR_ARROW:
1584             cursor_type = 1000; //PointerIcon.TYPE_ARROW;
1585             break;
1586         case SDL_SYSTEM_CURSOR_IBEAM:
1587             cursor_type = 1008; //PointerIcon.TYPE_TEXT;
1588             break;
1589         case SDL_SYSTEM_CURSOR_WAIT:
1590             cursor_type = 1004; //PointerIcon.TYPE_WAIT;
1591             break;
1592         case SDL_SYSTEM_CURSOR_CROSSHAIR:
1593             cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR;
1594             break;
1595         case SDL_SYSTEM_CURSOR_WAITARROW:
1596             cursor_type = 1004; //PointerIcon.TYPE_WAIT;
1597             break;
1598         case SDL_SYSTEM_CURSOR_SIZENWSE:
1599             cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
1600             break;
1601         case SDL_SYSTEM_CURSOR_SIZENESW:
1602             cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
1603             break;
1604         case SDL_SYSTEM_CURSOR_SIZEWE:
1605             cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
1606             break;
1607         case SDL_SYSTEM_CURSOR_SIZENS:
1608             cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
1609             break;
1610         case SDL_SYSTEM_CURSOR_SIZEALL:
1611             cursor_type = 1020; //PointerIcon.TYPE_GRAB;
1612             break;
1613         case SDL_SYSTEM_CURSOR_NO:
1614             cursor_type = 1012; //PointerIcon.TYPE_NO_DROP;
1615             break;
1616         case SDL_SYSTEM_CURSOR_HAND:
1617             cursor_type = 1002; //PointerIcon.TYPE_HAND;
1618             break;
1619         }
1620         if (Build.VERSION.SDK_INT >= 24) {
1621             try {
1622                 mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type));
1623             } catch (Exception e) {
1624                 return false;
1625             }
1626         }
1627         return true;
1628     }
1629 
1630     /**
1631      * This method is called by SDL using JNI.
1632      */
requestPermission(String permission, int requestCode)1633     public static void requestPermission(String permission, int requestCode) {
1634         if (Build.VERSION.SDK_INT < 23) {
1635             nativePermissionResult(requestCode, true);
1636             return;
1637         }
1638 
1639         Activity activity = (Activity)getContext();
1640         if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
1641             activity.requestPermissions(new String[]{permission}, requestCode);
1642         } else {
1643             nativePermissionResult(requestCode, true);
1644         }
1645     }
1646 
1647     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)1648     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
1649         if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
1650             nativePermissionResult(requestCode, true);
1651         } else {
1652             nativePermissionResult(requestCode, false);
1653         }
1654     }
1655 }
1656 
1657 /**
1658     Simple runnable to start the SDL application
1659 */
1660 class SDLMain implements Runnable {
1661     @Override
run()1662     public void run() {
1663         // Runs SDL_main()
1664         String library = SDLActivity.mSingleton.getMainSharedObject();
1665         String function = SDLActivity.mSingleton.getMainFunction();
1666         String[] arguments = SDLActivity.mSingleton.getArguments();
1667 
1668         try {
1669             android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY);
1670         } catch (Exception e) {
1671             Log.v("SDL", "modify thread properties failed " + e.toString());
1672         }
1673 
1674         Log.v("SDL", "Running main function " + function + " from library " + library);
1675 
1676         SDLActivity.nativeRunMain(library, function, arguments);
1677 
1678         Log.v("SDL", "Finished main function");
1679 
1680         if (SDLActivity.mSingleton == null || SDLActivity.mSingleton.isFinishing()) {
1681             // Activity is already being destroyed
1682         } else {
1683             // Let's finish the Activity
1684             SDLActivity.mSDLThread = null;
1685             SDLActivity.mSingleton.finish();
1686         }
1687     }
1688 }
1689 
1690 
1691 /**
1692     SDLSurface. This is what we draw on, so we need to know when it's created
1693     in order to do anything useful.
1694 
1695     Because of this, that's where we set up the SDL thread
1696 */
1697 class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
1698     View.OnKeyListener, View.OnTouchListener, SensorEventListener  {
1699 
1700     // Sensors
1701     protected SensorManager mSensorManager;
1702     protected Display mDisplay;
1703 
1704     // Keep track of the surface size to normalize touch events
1705     protected float mWidth, mHeight;
1706 
1707     // Is SurfaceView ready for rendering
1708     public boolean mIsSurfaceReady;
1709 
1710     // Startup
SDLSurface(Context context)1711     public SDLSurface(Context context) {
1712         super(context);
1713         getHolder().addCallback(this);
1714 
1715         setFocusable(true);
1716         setFocusableInTouchMode(true);
1717         requestFocus();
1718         setOnKeyListener(this);
1719         setOnTouchListener(this);
1720 
1721         mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
1722         mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
1723 
1724         setOnGenericMotionListener(SDLActivity.getMotionListener());
1725 
1726         // Some arbitrary defaults to avoid a potential division by zero
1727         mWidth = 1.0f;
1728         mHeight = 1.0f;
1729 
1730         mIsSurfaceReady = false;
1731     }
1732 
handlePause()1733     public void handlePause() {
1734         enableSensor(Sensor.TYPE_ACCELEROMETER, false);
1735     }
1736 
handleResume()1737     public void handleResume() {
1738         setFocusable(true);
1739         setFocusableInTouchMode(true);
1740         requestFocus();
1741         setOnKeyListener(this);
1742         setOnTouchListener(this);
1743         enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1744     }
1745 
getNativeSurface()1746     public Surface getNativeSurface() {
1747         return getHolder().getSurface();
1748     }
1749 
1750     // Called when we have a valid drawing surface
1751     @Override
surfaceCreated(SurfaceHolder holder)1752     public void surfaceCreated(SurfaceHolder holder) {
1753         Log.v("SDL", "surfaceCreated()");
1754         SDLActivity.onNativeSurfaceCreated();
1755     }
1756 
1757     // Called when we lose the surface
1758     @Override
surfaceDestroyed(SurfaceHolder holder)1759     public void surfaceDestroyed(SurfaceHolder holder) {
1760         Log.v("SDL", "surfaceDestroyed()");
1761 
1762         // Transition to pause, if needed
1763         SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
1764         SDLActivity.handleNativeState();
1765 
1766         mIsSurfaceReady = false;
1767         SDLActivity.onNativeSurfaceDestroyed();
1768     }
1769 
1770     // Called when the surface is resized
1771     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)1772     public void surfaceChanged(SurfaceHolder holder,
1773                                int format, int width, int height) {
1774         Log.v("SDL", "surfaceChanged()");
1775 
1776         if (SDLActivity.mSingleton == null) {
1777             return;
1778         }
1779 
1780         int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
1781         switch (format) {
1782         case PixelFormat.RGBA_8888:
1783             Log.v("SDL", "pixel format RGBA_8888");
1784             sdlFormat = 0x16462004; // SDL_PIXELFORMAT_RGBA8888
1785             break;
1786         case PixelFormat.RGBX_8888:
1787             Log.v("SDL", "pixel format RGBX_8888");
1788             sdlFormat = 0x16261804; // SDL_PIXELFORMAT_RGBX8888
1789             break;
1790         case PixelFormat.RGB_565:
1791             Log.v("SDL", "pixel format RGB_565");
1792             sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565
1793             break;
1794         case PixelFormat.RGB_888:
1795             Log.v("SDL", "pixel format RGB_888");
1796             // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead?
1797             sdlFormat = 0x16161804; // SDL_PIXELFORMAT_RGB888
1798             break;
1799         default:
1800             Log.v("SDL", "pixel format unknown " + format);
1801             break;
1802         }
1803 
1804         mWidth = width;
1805         mHeight = height;
1806         int nDeviceWidth = width;
1807         int nDeviceHeight = height;
1808         try
1809         {
1810             if (Build.VERSION.SDK_INT >= 17) {
1811                 android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics();
1812                 mDisplay.getRealMetrics( realMetrics );
1813                 nDeviceWidth = realMetrics.widthPixels;
1814                 nDeviceHeight = realMetrics.heightPixels;
1815             }
1816         }
1817         catch ( java.lang.Throwable throwable ) {}
1818 
1819         synchronized(SDLActivity.getContext()) {
1820             // In case we're waiting on a size change after going fullscreen, send a notification.
1821             SDLActivity.getContext().notifyAll();
1822         }
1823 
1824         Log.v("SDL", "Window size: " + width + "x" + height);
1825         Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
1826         SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, sdlFormat, mDisplay.getRefreshRate());
1827         SDLActivity.onNativeResize();
1828 
1829         // Prevent a screen distortion glitch,
1830         // for instance when the device is in Landscape and a Portrait App is resumed.
1831         boolean skip = false;
1832         int requestedOrientation = SDLActivity.mAllowSDLOrientationChanges ? SDLActivity.mSingleton.getRequestedOrientation() : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
1833 
1834         if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
1835         {
1836             // Accept any
1837         }
1838         else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT)
1839         {
1840             if (mWidth > mHeight) {
1841                skip = true;
1842             }
1843         } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
1844             if (mWidth < mHeight) {
1845                skip = true;
1846             }
1847         }
1848 
1849         // Special Patch for Square Resolution: Black Berry Passport
1850         if (skip) {
1851            double min = Math.min(mWidth, mHeight);
1852            double max = Math.max(mWidth, mHeight);
1853 
1854            if (max / min < 1.20) {
1855               Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
1856               skip = false;
1857            }
1858         }
1859 
1860         // Don't skip in MultiWindow.
1861         if (skip) {
1862             if (Build.VERSION.SDK_INT >= 24) {
1863                 if (SDLActivity.mSingleton.isInMultiWindowMode()) {
1864                     Log.v("SDL", "Don't skip in Multi-Window");
1865                     skip = false;
1866                 }
1867             }
1868         }
1869 
1870         if (skip) {
1871            Log.v("SDL", "Skip .. Surface is not ready.");
1872            mIsSurfaceReady = false;
1873            return;
1874         }
1875 
1876         /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
1877         SDLActivity.onNativeSurfaceChanged();
1878 
1879         /* Surface is ready */
1880         mIsSurfaceReady = true;
1881 
1882         SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
1883         SDLActivity.handleNativeState();
1884     }
1885 
1886     // Key events
1887     @Override
onKey(View v, int keyCode, KeyEvent event)1888     public boolean onKey(View  v, int keyCode, KeyEvent event) {
1889 
1890         int deviceId = event.getDeviceId();
1891         int source = event.getSource();
1892 
1893         // Dispatch the different events depending on where they come from
1894         // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
1895         // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
1896         //
1897         // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
1898         // SOURCE_JOYSTICK, while its key events arrive from the keyboard source
1899         // So, retrieve the device itself and check all of its sources
1900         if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) {
1901             // Note that we process events with specific key codes here
1902             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1903                 if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) {
1904                     return true;
1905                 }
1906             } else if (event.getAction() == KeyEvent.ACTION_UP) {
1907                 if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) {
1908                     return true;
1909                 }
1910             }
1911         }
1912 
1913         if (source == InputDevice.SOURCE_UNKNOWN) {
1914             InputDevice device = InputDevice.getDevice(deviceId);
1915             if (device != null) {
1916                 source = device.getSources();
1917             }
1918         }
1919 
1920         if ((source & InputDevice.SOURCE_KEYBOARD) != 0) {
1921             if (event.getAction() == KeyEvent.ACTION_DOWN) {
1922                 //Log.v("SDL", "key down: " + keyCode);
1923                 if (SDLActivity.isTextInputEvent(event)) {
1924                     SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
1925                 }
1926                 SDLActivity.onNativeKeyDown(keyCode);
1927                 return true;
1928             }
1929             else if (event.getAction() == KeyEvent.ACTION_UP) {
1930                 //Log.v("SDL", "key up: " + keyCode);
1931                 SDLActivity.onNativeKeyUp(keyCode);
1932                 return true;
1933             }
1934         }
1935 
1936         if ((source & InputDevice.SOURCE_MOUSE) != 0) {
1937             // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
1938             // they are ignored here because sending them as mouse input to SDL is messy
1939             if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
1940                 switch (event.getAction()) {
1941                 case KeyEvent.ACTION_DOWN:
1942                 case KeyEvent.ACTION_UP:
1943                     // mark the event as handled or it will be handled by system
1944                     // handling KEYCODE_BACK by system will call onBackPressed()
1945                     return true;
1946                 }
1947             }
1948         }
1949 
1950         return false;
1951     }
1952 
1953     // Touch events
1954     @Override
onTouch(View v, MotionEvent event)1955     public boolean onTouch(View v, MotionEvent event) {
1956         /* Ref: http://developer.android.com/training/gestures/multi.html */
1957         final int touchDevId = event.getDeviceId();
1958         final int pointerCount = event.getPointerCount();
1959         int action = event.getActionMasked();
1960         int pointerFingerId;
1961         int mouseButton;
1962         int i = -1;
1963         float x,y,p;
1964 
1965         // 12290 = Samsung DeX mode desktop mouse
1966         // 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
1967         // 0x2   = SOURCE_CLASS_POINTER
1968         if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
1969             try {
1970                 mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event);
1971             } catch(Exception e) {
1972                 mouseButton = 1;    // oh well.
1973             }
1974 
1975             // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
1976             // if we are.  We'll leverage our existing mouse motion listener
1977             SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
1978             x = motionListener.getEventX(event);
1979             y = motionListener.getEventY(event);
1980 
1981             SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
1982         } else {
1983             switch(action) {
1984                 case MotionEvent.ACTION_MOVE:
1985                     for (i = 0; i < pointerCount; i++) {
1986                         pointerFingerId = event.getPointerId(i);
1987                         x = event.getX(i) / mWidth;
1988                         y = event.getY(i) / mHeight;
1989                         p = event.getPressure(i);
1990                         if (p > 1.0f) {
1991                             // may be larger than 1.0f on some devices
1992                             // see the documentation of getPressure(i)
1993                             p = 1.0f;
1994                         }
1995                         SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1996                     }
1997                     break;
1998 
1999                 case MotionEvent.ACTION_UP:
2000                 case MotionEvent.ACTION_DOWN:
2001                     // Primary pointer up/down, the index is always zero
2002                     i = 0;
2003                 case MotionEvent.ACTION_POINTER_UP:
2004                 case MotionEvent.ACTION_POINTER_DOWN:
2005                     // Non primary pointer up/down
2006                     if (i == -1) {
2007                         i = event.getActionIndex();
2008                     }
2009 
2010                     pointerFingerId = event.getPointerId(i);
2011                     x = event.getX(i) / mWidth;
2012                     y = event.getY(i) / mHeight;
2013                     p = event.getPressure(i);
2014                     if (p > 1.0f) {
2015                         // may be larger than 1.0f on some devices
2016                         // see the documentation of getPressure(i)
2017                         p = 1.0f;
2018                     }
2019                     SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
2020                     break;
2021 
2022                 case MotionEvent.ACTION_CANCEL:
2023                     for (i = 0; i < pointerCount; i++) {
2024                         pointerFingerId = event.getPointerId(i);
2025                         x = event.getX(i) / mWidth;
2026                         y = event.getY(i) / mHeight;
2027                         p = event.getPressure(i);
2028                         if (p > 1.0f) {
2029                             // may be larger than 1.0f on some devices
2030                             // see the documentation of getPressure(i)
2031                             p = 1.0f;
2032                         }
2033                         SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
2034                     }
2035                     break;
2036 
2037                 default:
2038                     break;
2039             }
2040         }
2041 
2042         return true;
2043    }
2044 
2045     // Sensor events
enableSensor(int sensortype, boolean enabled)2046     public void enableSensor(int sensortype, boolean enabled) {
2047         // TODO: This uses getDefaultSensor - what if we have >1 accels?
2048         if (enabled) {
2049             mSensorManager.registerListener(this,
2050                             mSensorManager.getDefaultSensor(sensortype),
2051                             SensorManager.SENSOR_DELAY_GAME, null);
2052         } else {
2053             mSensorManager.unregisterListener(this,
2054                             mSensorManager.getDefaultSensor(sensortype));
2055         }
2056     }
2057 
2058     @Override
onAccuracyChanged(Sensor sensor, int accuracy)2059     public void onAccuracyChanged(Sensor sensor, int accuracy) {
2060         // TODO
2061     }
2062 
2063     @Override
onSensorChanged(SensorEvent event)2064     public void onSensorChanged(SensorEvent event) {
2065         if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
2066 
2067             // Since we may have an orientation set, we won't receive onConfigurationChanged events.
2068             // We thus should check here.
2069             int newOrientation = SDLActivity.SDL_ORIENTATION_UNKNOWN;
2070 
2071             float x, y;
2072             switch (mDisplay.getRotation()) {
2073                 case Surface.ROTATION_90:
2074                     x = -event.values[1];
2075                     y = event.values[0];
2076                     newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
2077                     break;
2078                 case Surface.ROTATION_270:
2079                     x = event.values[1];
2080                     y = -event.values[0];
2081                     newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
2082                     break;
2083                 case Surface.ROTATION_180:
2084                     x = -event.values[0];
2085                     y = -event.values[1];
2086                     newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
2087                     break;
2088                 default:
2089                     x = event.values[0];
2090                     y = event.values[1];
2091                     newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
2092                     break;
2093             }
2094 
2095             if (newOrientation != SDLActivity.mCurrentOrientation) {
2096                 SDLActivity.mCurrentOrientation = newOrientation;
2097                 SDLActivity.onNativeOrientationChanged(newOrientation);
2098             }
2099 
2100             SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
2101                                       y / SensorManager.GRAVITY_EARTH,
2102                                       event.values[2] / SensorManager.GRAVITY_EARTH);
2103 
2104 
2105         }
2106     }
2107 
2108     // Captured pointer events for API 26.
onCapturedPointerEvent(MotionEvent event)2109     public boolean onCapturedPointerEvent(MotionEvent event)
2110     {
2111         int action = event.getActionMasked();
2112 
2113         float x, y;
2114         switch (action) {
2115             case MotionEvent.ACTION_SCROLL:
2116                 x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
2117                 y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
2118                 SDLActivity.onNativeMouse(0, action, x, y, false);
2119                 return true;
2120 
2121             case MotionEvent.ACTION_HOVER_MOVE:
2122             case MotionEvent.ACTION_MOVE:
2123                 x = event.getX(0);
2124                 y = event.getY(0);
2125                 SDLActivity.onNativeMouse(0, action, x, y, true);
2126                 return true;
2127 
2128             case MotionEvent.ACTION_BUTTON_PRESS:
2129             case MotionEvent.ACTION_BUTTON_RELEASE:
2130 
2131                 // Change our action value to what SDL's code expects.
2132                 if (action == MotionEvent.ACTION_BUTTON_PRESS) {
2133                     action = MotionEvent.ACTION_DOWN;
2134                 }
2135                 else if (action == MotionEvent.ACTION_BUTTON_RELEASE) {
2136                     action = MotionEvent.ACTION_UP;
2137                 }
2138 
2139                 x = event.getX(0);
2140                 y = event.getY(0);
2141                 int button = event.getButtonState();
2142 
2143                 SDLActivity.onNativeMouse(button, action, x, y, true);
2144                 return true;
2145         }
2146 
2147         return false;
2148     }
2149 
2150 }
2151 
2152 /* This is a fake invisible editor view that receives the input and defines the
2153  * pan&scan region
2154  */
2155 class DummyEdit extends View implements View.OnKeyListener {
2156     InputConnection ic;
2157 
DummyEdit(Context context)2158     public DummyEdit(Context context) {
2159         super(context);
2160         setFocusableInTouchMode(true);
2161         setFocusable(true);
2162         setOnKeyListener(this);
2163     }
2164 
2165     @Override
onCheckIsTextEditor()2166     public boolean onCheckIsTextEditor() {
2167         return true;
2168     }
2169 
2170     @Override
onKey(View v, int keyCode, KeyEvent event)2171     public boolean onKey(View v, int keyCode, KeyEvent event) {
2172         /*
2173          * This handles the hardware keyboard input
2174          */
2175         if (event.getAction() == KeyEvent.ACTION_DOWN) {
2176             if (SDLActivity.isTextInputEvent(event)) {
2177                 ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
2178                 return true;
2179             }
2180             SDLActivity.onNativeKeyDown(keyCode);
2181             return true;
2182         } else if (event.getAction() == KeyEvent.ACTION_UP) {
2183             SDLActivity.onNativeKeyUp(keyCode);
2184             return true;
2185         }
2186         return false;
2187     }
2188 
2189     //
2190     @Override
onKeyPreIme(int keyCode, KeyEvent event)2191     public boolean onKeyPreIme (int keyCode, KeyEvent event) {
2192         // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
2193         // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
2194         // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
2195         // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
2196         // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
2197         // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
2198         if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
2199             if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
2200                 SDLActivity.onNativeKeyboardFocusLost();
2201             }
2202         }
2203         return super.onKeyPreIme(keyCode, event);
2204     }
2205 
2206     @Override
onCreateInputConnection(EditorInfo outAttrs)2207     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
2208         ic = new SDLInputConnection(this, true);
2209 
2210         outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
2211         outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
2212                 | EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
2213 
2214         return ic;
2215     }
2216 }
2217 
2218 class SDLInputConnection extends BaseInputConnection {
2219 
SDLInputConnection(View targetView, boolean fullEditor)2220     public SDLInputConnection(View targetView, boolean fullEditor) {
2221         super(targetView, fullEditor);
2222 
2223     }
2224 
2225     @Override
sendKeyEvent(KeyEvent event)2226     public boolean sendKeyEvent(KeyEvent event) {
2227         /*
2228          * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
2229          * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
2230          * and so we need to generate them ourselves in commitText.  To avoid duplicates on the handful of keys
2231          * that still do, we empty this out.
2232          */
2233 
2234         /*
2235          * Return DOES still generate a key event, however.  So rather than using it as the 'click a button' key
2236          * as we do with physical keyboards, let's just use it to hide the keyboard.
2237          */
2238 
2239         if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
2240             if (SDLActivity.onNativeSoftReturnKey()) {
2241                 return true;
2242             }
2243         }
2244 
2245 
2246         return super.sendKeyEvent(event);
2247     }
2248 
2249     @Override
commitText(CharSequence text, int newCursorPosition)2250     public boolean commitText(CharSequence text, int newCursorPosition) {
2251 
2252         for (int i = 0; i < text.length(); i++) {
2253             char c = text.charAt(i);
2254             if (c == '\n') {
2255                 if (SDLActivity.onNativeSoftReturnKey()) {
2256                     return true;
2257                 }
2258             }
2259             nativeGenerateScancodeForUnichar(c);
2260         }
2261 
2262         SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition);
2263 
2264         return super.commitText(text, newCursorPosition);
2265     }
2266 
2267     @Override
setComposingText(CharSequence text, int newCursorPosition)2268     public boolean setComposingText(CharSequence text, int newCursorPosition) {
2269 
2270         nativeSetComposingText(text.toString(), newCursorPosition);
2271 
2272         return super.setComposingText(text, newCursorPosition);
2273     }
2274 
nativeCommitText(String text, int newCursorPosition)2275     public static native void nativeCommitText(String text, int newCursorPosition);
2276 
nativeGenerateScancodeForUnichar(char c)2277     public native void nativeGenerateScancodeForUnichar(char c);
2278 
nativeSetComposingText(String text, int newCursorPosition)2279     public native void nativeSetComposingText(String text, int newCursorPosition);
2280 
2281     @Override
deleteSurroundingText(int beforeLength, int afterLength)2282     public boolean deleteSurroundingText(int beforeLength, int afterLength) {
2283         // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
2284         // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
2285         if (beforeLength > 0 && afterLength == 0) {
2286             boolean ret = true;
2287             // backspace(s)
2288             while (beforeLength-- > 0) {
2289                boolean ret_key = sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
2290                               && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
2291                ret = ret && ret_key;
2292             }
2293             return ret;
2294         }
2295 
2296         return super.deleteSurroundingText(beforeLength, afterLength);
2297     }
2298 }
2299 
2300 interface SDLClipboardHandler {
2301 
clipboardHasText()2302     public boolean clipboardHasText();
clipboardGetText()2303     public String clipboardGetText();
clipboardSetText(String string)2304     public void clipboardSetText(String string);
2305 
2306 }
2307 
2308 
2309 class SDLClipboardHandler_API11 implements
2310     SDLClipboardHandler,
2311     android.content.ClipboardManager.OnPrimaryClipChangedListener {
2312 
2313     protected android.content.ClipboardManager mClipMgr;
2314 
SDLClipboardHandler_API11()2315     SDLClipboardHandler_API11() {
2316        mClipMgr = (android.content.ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
2317        mClipMgr.addPrimaryClipChangedListener(this);
2318     }
2319 
2320     @Override
clipboardHasText()2321     public boolean clipboardHasText() {
2322        return mClipMgr.hasText();
2323     }
2324 
2325     @Override
clipboardGetText()2326     public String clipboardGetText() {
2327         CharSequence text;
2328         text = mClipMgr.getText();
2329         if (text != null) {
2330            return text.toString();
2331         }
2332         return null;
2333     }
2334 
2335     @Override
clipboardSetText(String string)2336     public void clipboardSetText(String string) {
2337        mClipMgr.removePrimaryClipChangedListener(this);
2338        mClipMgr.setText(string);
2339        mClipMgr.addPrimaryClipChangedListener(this);
2340     }
2341 
2342     @Override
onPrimaryClipChanged()2343     public void onPrimaryClipChanged() {
2344         SDLActivity.onNativeClipboardChanged();
2345     }
2346 
2347 }
2348 
2349