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