1 package org.libsdl.app;
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.util.Arrays;
6 import java.util.Hashtable;
7 import java.lang.reflect.Method;
11 import android.content.*;
12 import android.content.res.Configuration;
13 import android.text.InputType;
14 import android.view.*;
15 import android.view.inputmethod.BaseInputConnection;
16 import android.view.inputmethod.EditorInfo;
17 import android.view.inputmethod.InputConnection;
18 import android.view.inputmethod.InputMethodManager;
19 import android.widget.RelativeLayout;
20 import android.widget.Button;
21 import android.widget.LinearLayout;
22 import android.widget.TextView;
24 import android.util.DisplayMetrics;
25 import android.util.Log;
26 import android.util.SparseArray;
27 import android.graphics.*;
28 import android.graphics.drawable.Drawable;
29 import android.hardware.*;
30 import android.content.pm.ActivityInfo;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ApplicationInfo;
37 public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
38 private static final String TAG = "SDL";
40 public static boolean mIsResumedCalled, mHasFocus;
41 public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24);
44 private static final int SDL_SYSTEM_CURSOR_NONE = -1;
45 private static final int SDL_SYSTEM_CURSOR_ARROW = 0;
46 private static final int SDL_SYSTEM_CURSOR_IBEAM = 1;
47 private static final int SDL_SYSTEM_CURSOR_WAIT = 2;
48 private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3;
49 private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4;
50 private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5;
51 private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6;
52 private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7;
53 private static final int SDL_SYSTEM_CURSOR_SIZENS = 8;
54 private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9;
55 private static final int SDL_SYSTEM_CURSOR_NO = 10;
56 private static final int SDL_SYSTEM_CURSOR_HAND = 11;
58 protected static final int SDL_ORIENTATION_UNKNOWN = 0;
59 protected static final int SDL_ORIENTATION_LANDSCAPE = 1;
60 protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2;
61 protected static final int SDL_ORIENTATION_PORTRAIT = 3;
62 protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4;
64 protected static int mCurrentOrientation;
66 // Handle the state of the native layer
67 public enum NativeState {
71 public static NativeState mNextNativeState;
72 public static NativeState mCurrentNativeState;
74 /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
75 public static boolean mBrokenLibraries;
78 protected static SDLActivity mSingleton;
79 protected static SDLSurface mSurface;
80 protected static View mTextEdit;
81 protected static boolean mScreenKeyboardShown;
82 protected static ViewGroup mLayout;
83 protected static SDLClipboardHandler mClipboardHandler;
84 protected static Hashtable<Integer, PointerIcon> mCursors;
85 protected static int mLastCursorID;
86 protected static SDLGenericMotionListener_API12 mMotionListener;
87 protected static HIDDeviceManager mHIDDeviceManager;
89 // This is what SDL runs in. It invokes SDL_main(), eventually
90 protected static Thread mSDLThread;
92 protected static SDLGenericMotionListener_API12 getMotionListener() {
93 if (mMotionListener == null) {
94 if (Build.VERSION.SDK_INT >= 26) {
95 mMotionListener = new SDLGenericMotionListener_API26();
97 if (Build.VERSION.SDK_INT >= 24) {
98 mMotionListener = new SDLGenericMotionListener_API24();
100 mMotionListener = new SDLGenericMotionListener_API12();
104 return mMotionListener;
108 * This method returns the name of the shared object with the application entry point
109 * It can be overridden by derived classes.
111 protected String getMainSharedObject() {
113 String[] libraries = SDLActivity.mSingleton.getLibraries();
114 if (libraries.length > 0) {
115 library = "lib" + libraries[libraries.length - 1] + ".so";
117 library = "libmain.so";
119 return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
123 * This method returns the name of the application entry point
124 * It can be overridden by derived classes.
126 protected String getMainFunction() {
131 * This method is called by SDL before loading the native shared libraries.
132 * It can be overridden to provide names of shared libraries to be loaded.
133 * The default implementation returns the defaults. It never returns null.
134 * An array returned by a new implementation must at least contain "SDL2".
135 * Also keep in mind that the order the libraries are loaded may matter.
136 * @return names of shared libraries to be loaded (e.g. "SDL2", "main").
138 protected String[] getLibraries() {
139 return new String[] {
151 public void loadLibraries() {
152 for (String lib : getLibraries()) {
153 SDL.loadLibrary(lib);
158 * This method is called by SDL before starting the native application thread.
159 * It can be overridden to provide the arguments after the application name.
160 * The default implementation returns an empty array. It never returns null.
161 * @return arguments for the native application.
163 protected String[] getArguments() {
164 return new String[0];
167 public static void initialize() {
168 // The static nature of the singleton and Android quirkyness force us to initialize everything here
169 // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values
174 mClipboardHandler = null;
175 mCursors = new Hashtable<Integer, PointerIcon>();
178 mBrokenLibraries = false;
179 mIsResumedCalled = false;
181 mNextNativeState = NativeState.INIT;
182 mCurrentNativeState = NativeState.INIT;
187 protected void onCreate(Bundle savedInstanceState) {
188 Log.v(TAG, "Device: " + Build.DEVICE);
189 Log.v(TAG, "Model: " + Build.MODEL);
190 Log.v(TAG, "onCreate()");
191 super.onCreate(savedInstanceState);
194 Thread.currentThread().setName("SDLActivity");
195 } catch (Exception e) {
196 Log.v(TAG, "modify thread properties failed " + e.toString());
199 // Load shared libraries
200 String errorMsgBrokenLib = "";
203 } catch(UnsatisfiedLinkError e) {
204 System.err.println(e.getMessage());
205 mBrokenLibraries = true;
206 errorMsgBrokenLib = e.getMessage();
207 } catch(Exception e) {
208 System.err.println(e.getMessage());
209 mBrokenLibraries = true;
210 errorMsgBrokenLib = e.getMessage();
213 if (mBrokenLibraries)
216 AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this);
217 dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall."
218 + System.getProperty("line.separator")
219 + System.getProperty("line.separator")
220 + "Error: " + errorMsgBrokenLib);
221 dlgAlert.setTitle("SDL Error");
222 dlgAlert.setPositiveButton("Exit",
223 new DialogInterface.OnClickListener() {
225 public void onClick(DialogInterface dialog,int id) {
226 // if this button is clicked, close current activity
227 SDLActivity.mSingleton.finish();
230 dlgAlert.setCancelable(false);
231 dlgAlert.create().show();
242 // So we can call stuff from static callbacks
244 SDL.setContext(this);
246 mClipboardHandler = new SDLClipboardHandler_API11();
248 mHIDDeviceManager = HIDDeviceManager.acquire(this);
250 // Set up the surface
251 mSurface = new SDLSurface(getApplication());
253 mLayout = new RelativeLayout(this);
254 mLayout.addView(mSurface);
256 // Get our current screen orientation and pass it down.
257 mCurrentOrientation = SDLActivity.getCurrentOrientation();
258 // Only record current orientation
259 SDLActivity.onNativeOrientationChanged(mCurrentOrientation);
261 setContentView(mLayout);
263 setWindowStyle(false);
265 getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
267 // Get filename from "Open with" of another application
268 Intent intent = getIntent();
269 if (intent != null && intent.getData() != null) {
270 String filename = intent.getData().getPath();
271 if (filename != null) {
272 Log.v(TAG, "Got filename: " + filename);
273 SDLActivity.onNativeDropFile(filename);
278 protected void pauseNativeThread() {
279 mNextNativeState = NativeState.PAUSED;
280 mIsResumedCalled = false;
282 if (SDLActivity.mBrokenLibraries) {
286 SDLActivity.handleNativeState();
289 protected void resumeNativeThread() {
290 mNextNativeState = NativeState.RESUMED;
291 mIsResumedCalled = true;
293 if (SDLActivity.mBrokenLibraries) {
297 SDLActivity.handleNativeState();
302 protected void onPause() {
303 Log.v(TAG, "onPause()");
306 if (mHIDDeviceManager != null) {
307 mHIDDeviceManager.setFrozen(true);
309 if (!mHasMultiWindow) {
315 protected void onResume() {
316 Log.v(TAG, "onResume()");
319 if (mHIDDeviceManager != null) {
320 mHIDDeviceManager.setFrozen(false);
322 if (!mHasMultiWindow) {
323 resumeNativeThread();
328 protected void onStop() {
329 Log.v(TAG, "onStop()");
331 if (mHasMultiWindow) {
337 protected void onStart() {
338 Log.v(TAG, "onStart()");
340 if (mHasMultiWindow) {
341 resumeNativeThread();
345 public static int getCurrentOrientation() {
346 final Context context = SDLActivity.getContext();
347 final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
349 int result = SDL_ORIENTATION_UNKNOWN;
351 switch (display.getRotation()) {
352 case Surface.ROTATION_0:
353 result = SDL_ORIENTATION_PORTRAIT;
356 case Surface.ROTATION_90:
357 result = SDL_ORIENTATION_LANDSCAPE;
360 case Surface.ROTATION_180:
361 result = SDL_ORIENTATION_PORTRAIT_FLIPPED;
364 case Surface.ROTATION_270:
365 result = SDL_ORIENTATION_LANDSCAPE_FLIPPED;
373 public void onWindowFocusChanged(boolean hasFocus) {
374 super.onWindowFocusChanged(hasFocus);
375 Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
377 if (SDLActivity.mBrokenLibraries) {
381 mHasFocus = hasFocus;
383 mNextNativeState = NativeState.RESUMED;
384 SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded();
386 SDLActivity.handleNativeState();
387 nativeFocusChanged(true);
390 nativeFocusChanged(false);
391 if (!mHasMultiWindow) {
392 mNextNativeState = NativeState.PAUSED;
393 SDLActivity.handleNativeState();
399 public void onLowMemory() {
400 Log.v(TAG, "onLowMemory()");
403 if (SDLActivity.mBrokenLibraries) {
407 SDLActivity.nativeLowMemory();
411 protected void onDestroy() {
412 Log.v(TAG, "onDestroy()");
414 if (mHIDDeviceManager != null) {
415 HIDDeviceManager.release(mHIDDeviceManager);
416 mHIDDeviceManager = null;
419 if (SDLActivity.mBrokenLibraries) {
424 if (SDLActivity.mSDLThread != null) {
426 // Send Quit event to "SDLThread" thread
427 SDLActivity.nativeSendQuit();
429 // Wait for "SDLThread" thread to end
431 SDLActivity.mSDLThread.join();
432 } catch(Exception e) {
433 Log.v(TAG, "Problem stopping SDLThread: " + e);
437 SDLActivity.nativeQuit();
443 public void onBackPressed() {
444 // Check if we want to block the back button in case of mouse right click.
446 // If we do, the normal hardware back button will no longer work and people have to use home,
447 // but the mouse right click will work.
449 String trapBack = SDLActivity.nativeGetHint("SDL_ANDROID_TRAP_BACK_BUTTON");
450 if ((trapBack != null) && trapBack.equals("1")) {
451 // Exit and let the mouse handler handle this button (if appropriate)
455 // Default system back button behavior.
456 if (!isFinishing()) {
457 super.onBackPressed();
461 // Called by JNI from SDL.
462 public static void manualBackButton() {
463 mSingleton.pressBackButton();
466 // Used to get us onto the activity's main thread
467 public void pressBackButton() {
468 runOnUiThread(new Runnable() {
471 if (!SDLActivity.this.isFinishing()) {
472 SDLActivity.this.superOnBackPressed();
478 // Used to access the system back behavior.
479 public void superOnBackPressed() {
480 super.onBackPressed();
484 public boolean dispatchKeyEvent(KeyEvent event) {
486 if (SDLActivity.mBrokenLibraries) {
490 int keyCode = event.getKeyCode();
491 // Ignore certain special keys so they're handled by Android
492 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
493 keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
494 keyCode == KeyEvent.KEYCODE_CAMERA ||
495 keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */
496 keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */
500 return super.dispatchKeyEvent(event);
503 /* Transition to next state */
504 public static void handleNativeState() {
506 if (mNextNativeState == mCurrentNativeState) {
507 // Already in same state, discard.
511 // Try a transition to init state
512 if (mNextNativeState == NativeState.INIT) {
514 mCurrentNativeState = mNextNativeState;
518 // Try a transition to paused state
519 if (mNextNativeState == NativeState.PAUSED) {
520 if (mSDLThread != null) {
523 if (mSurface != null) {
524 mSurface.handlePause();
526 mCurrentNativeState = mNextNativeState;
530 // Try a transition to resumed state
531 if (mNextNativeState == NativeState.RESUMED) {
532 if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) {
533 if (mSDLThread == null) {
534 // This is the entry point to the C app.
535 // Start up the C app thread and enable sensor input for the first time
536 // FIXME: Why aren't we enabling sensor input at start?
538 mSDLThread = new Thread(new SDLMain(), "SDLThread");
539 mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true);
542 // No nativeResume(), don't signal Android_ResumeSem
543 mSurface.handleResume();
546 mSurface.handleResume();
549 mCurrentNativeState = mNextNativeState;
554 // Messages from the SDLMain thread
555 static final int COMMAND_CHANGE_TITLE = 1;
556 static final int COMMAND_CHANGE_WINDOW_STYLE = 2;
557 static final int COMMAND_TEXTEDIT_HIDE = 3;
558 static final int COMMAND_CHANGE_SURFACEVIEW_FORMAT = 4;
559 static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
561 protected static final int COMMAND_USER = 0x8000;
563 protected static boolean mFullscreenModeActive;
566 * This method is called by SDL if SDL did not handle a message itself.
567 * This happens if a received message contains an unsupported command.
568 * Method can be overwritten to handle Messages in a different class.
569 * @param command the command of the message.
570 * @param param the parameter of the message. May be null.
571 * @return if the message was handled in overridden method.
573 protected boolean onUnhandledMessage(int command, Object param) {
578 * A Handler class for Messages from native SDL applications.
579 * It uses current Activities as target (e.g. for the title).
580 * static to prevent implicit references to enclosing object.
582 protected static class SDLCommandHandler extends Handler {
584 public void handleMessage(Message msg) {
585 Context context = SDL.getContext();
586 if (context == null) {
587 Log.e(TAG, "error handling message, getContext() returned null");
591 case COMMAND_CHANGE_TITLE:
592 if (context instanceof Activity) {
593 ((Activity) context).setTitle((String)msg.obj);
595 Log.e(TAG, "error handling message, getContext() returned no Activity");
598 case COMMAND_CHANGE_WINDOW_STYLE:
599 if (Build.VERSION.SDK_INT < 19) {
600 // This version of Android doesn't support the immersive fullscreen mode
603 if (context instanceof Activity) {
604 Window window = ((Activity) context).getWindow();
605 if (window != null) {
606 if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
607 int flags = View.SYSTEM_UI_FLAG_FULLSCREEN |
608 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
609 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
610 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
611 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
612 View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE;
613 window.getDecorView().setSystemUiVisibility(flags);
614 window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
615 window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
616 SDLActivity.mFullscreenModeActive = true;
618 int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE;
619 window.getDecorView().setSystemUiVisibility(flags);
620 window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
621 window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
622 SDLActivity.mFullscreenModeActive = false;
626 Log.e(TAG, "error handling message, getContext() returned no Activity");
629 case COMMAND_TEXTEDIT_HIDE:
630 if (mTextEdit != null) {
631 // Note: On some devices setting view to GONE creates a flicker in landscape.
632 // Setting the View's sizes to 0 is similar to GONE but without the flicker.
633 // The sizes will be set to useful values when the keyboard is shown again.
634 mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0));
636 InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
637 imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
639 mScreenKeyboardShown = false;
641 mSurface.requestFocus();
644 case COMMAND_SET_KEEP_SCREEN_ON:
646 if (context instanceof Activity) {
647 Window window = ((Activity) context).getWindow();
648 if (window != null) {
649 if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) {
650 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
652 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
658 case COMMAND_CHANGE_SURFACEVIEW_FORMAT:
660 int format = (Integer) msg.obj;
663 if (SDLActivity.mSurface == null) {
667 SurfaceHolder holder = SDLActivity.mSurface.getHolder();
668 if (holder == null) {
673 pf = PixelFormat.RGBA_8888;
674 } else if (format == 2) {
675 pf = PixelFormat.RGBX_8888;
677 pf = PixelFormat.RGB_565;
680 holder.setFormat(pf);
685 if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
686 Log.e(TAG, "error handling message, command is " + msg.arg1);
692 // Handler for the messages
693 Handler commandHandler = new SDLCommandHandler();
695 // Send a message from the SDLMain thread
696 boolean sendCommand(int command, Object data) {
697 Message msg = commandHandler.obtainMessage();
700 boolean result = commandHandler.sendMessage(msg);
702 if ((Build.VERSION.SDK_INT >= 19) && (command == COMMAND_CHANGE_WINDOW_STYLE)) {
703 // Ensure we don't return until the resize has actually happened,
704 // or 500ms have passed.
706 boolean bShouldWait = false;
708 if (data instanceof Integer) {
709 // Let's figure out if we're already laid out fullscreen or not.
710 Display display = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
711 android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics();
712 display.getRealMetrics( realMetrics );
714 boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) &&
715 (realMetrics.heightPixels == mSurface.getHeight()));
717 if (((Integer)data).intValue() == 1) {
718 // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going
719 // to change size and should wait for surfaceChanged() before we return, so the size
720 // is right back in native code. If we're already laid out fullscreen, though, we're
721 // not going to change size even if we change decor modes, so we shouldn't wait for
722 // surfaceChanged() -- which may not even happen -- and should return immediately.
723 bShouldWait = !bFullscreenLayout;
726 // If we're laid out fullscreen (even if the status bar and nav bar are present),
727 // or are actively in fullscreen, we're going to change size and should wait for
728 // surfaceChanged before we return, so the size is right back in native code.
729 bShouldWait = bFullscreenLayout;
733 if (bShouldWait && (SDLActivity.getContext() != null)) {
734 // We'll wait for the surfaceChanged() method, which will notify us
735 // when called. That way, we know our current size is really the
736 // size we need, instead of grabbing a size that's still got
737 // the navigation and/or status bars before they're hidden.
739 // We'll wait for up to half a second, because some devices
740 // take a surprisingly long time for the surface resize, but
741 // then we'll just give up and return.
743 synchronized(SDLActivity.getContext()) {
745 SDLActivity.getContext().wait(500);
747 catch (InterruptedException ie) {
748 ie.printStackTrace();
757 // C functions we call
758 public static native int nativeSetupJNI();
759 public static native int nativeRunMain(String library, String function, Object arguments);
760 public static native void nativeLowMemory();
761 public static native void nativeSendQuit();
762 public static native void nativeQuit();
763 public static native void nativePause();
764 public static native void nativeResume();
765 public static native void nativeFocusChanged(boolean hasFocus);
766 public static native void onNativeDropFile(String filename);
767 public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, int format, float rate);
768 public static native void onNativeResize();
769 public static native void onNativeKeyDown(int keycode);
770 public static native void onNativeKeyUp(int keycode);
771 public static native boolean onNativeSoftReturnKey();
772 public static native void onNativeKeyboardFocusLost();
773 public static native void onNativeMouse(int button, int action, float x, float y, boolean relative);
774 public static native void onNativeTouch(int touchDevId, int pointerFingerId,
777 public static native void onNativeAccel(float x, float y, float z);
778 public static native void onNativeClipboardChanged();
779 public static native void onNativeSurfaceCreated();
780 public static native void onNativeSurfaceChanged();
781 public static native void onNativeSurfaceDestroyed();
782 public static native String nativeGetHint(String name);
783 public static native void nativeSetenv(String name, String value);
784 public static native void onNativeOrientationChanged(int orientation);
785 public static native void nativeAddTouch(int touchId, String name);
786 public static native void nativePermissionResult(int requestCode, boolean result);
789 * This method is called by SDL using JNI.
791 public static boolean setActivityTitle(String title) {
792 // Called from SDLMain() thread and can't directly affect the view
793 return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
797 * This method is called by SDL using JNI.
799 public static void setWindowStyle(boolean fullscreen) {
800 // Called from SDLMain() thread and can't directly affect the view
801 mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0);
805 * This method is called by SDL using JNI.
806 * This is a static method for JNI convenience, it calls a non-static method
807 * so that is can be overridden
809 public static void setOrientation(int w, int h, boolean resizable, String hint)
811 if (mSingleton != null) {
812 mSingleton.setOrientationBis(w, h, resizable, hint);
817 * This can be overridden
819 public void setOrientationBis(int w, int h, boolean resizable, String hint)
821 int orientation_landscape = -1;
822 int orientation_portrait = -1;
824 /* If set, hint "explicitly controls which UI orientations are allowed". */
825 if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) {
826 orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
827 } else if (hint.contains("LandscapeRight")) {
828 orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
829 } else if (hint.contains("LandscapeLeft")) {
830 orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
833 if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) {
834 orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
835 } else if (hint.contains("Portrait")) {
836 orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
837 } else if (hint.contains("PortraitUpsideDown")) {
838 orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
841 boolean is_landscape_allowed = (orientation_landscape == -1 ? false : true);
842 boolean is_portrait_allowed = (orientation_portrait == -1 ? false : true);
843 int req = -1; /* Requested orientation */
845 /* No valid hint, nothing is explicitly allowed */
846 if (!is_portrait_allowed && !is_landscape_allowed) {
848 /* All orientations are allowed */
849 req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
851 /* Fixed window and nothing specified. Get orientation from w/h of created window */
852 req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
855 /* At least one orientation is allowed */
857 if (is_portrait_allowed && is_landscape_allowed) {
858 /* hint allows both landscape and portrait, promote to full sensor */
859 req = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
861 /* Use the only one allowed "orientation" */
862 req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
865 /* Fixed window and both orientations are allowed. Choose one. */
866 if (is_portrait_allowed && is_landscape_allowed) {
867 req = (w > h ? orientation_landscape : orientation_portrait);
869 /* Use the only one allowed "orientation" */
870 req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
875 Log.v("SDL", "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint);
876 mSingleton.setRequestedOrientation(req);
880 * This method is called by SDL using JNI.
882 public static void minimizeWindow() {
884 if (mSingleton == null) {
888 Intent startMain = new Intent(Intent.ACTION_MAIN);
889 startMain.addCategory(Intent.CATEGORY_HOME);
890 startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
891 mSingleton.startActivity(startMain);
895 * This method is called by SDL using JNI.
897 public static boolean shouldMinimizeOnFocusLoss() {
899 if (Build.VERSION.SDK_INT >= 24) {
900 if (mSingleton == null) {
904 if (mSingleton.isInMultiWindowMode()) {
908 if (mSingleton.isInPictureInPictureMode()) {
919 * This method is called by SDL using JNI.
921 public static boolean isScreenKeyboardShown()
923 if (mTextEdit == null) {
927 if (!mScreenKeyboardShown) {
931 InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
932 return imm.isAcceptingText();
937 * This method is called by SDL using JNI.
939 public static boolean supportsRelativeMouse()
941 // ChromeOS doesn't provide relative mouse motion via the Android 7 APIs
942 if (isChromebook()) {
946 // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under
947 // Android 7 APIs, and simply returns no data under Android 8 APIs.
949 // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and
950 // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result,
951 // we should stick to relative mode.
953 if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) {
957 return SDLActivity.getMotionListener().supportsRelativeMouse();
961 * This method is called by SDL using JNI.
963 public static boolean setRelativeMouseEnabled(boolean enabled)
965 if (enabled && !supportsRelativeMouse()) {
969 return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled);
973 * This method is called by SDL using JNI.
975 public static boolean sendMessage(int command, int param) {
976 if (mSingleton == null) {
979 return mSingleton.sendCommand(command, Integer.valueOf(param));
983 * This method is called by SDL using JNI.
985 public static Context getContext() {
986 return SDL.getContext();
990 * This method is called by SDL using JNI.
992 public static boolean isAndroidTV() {
993 UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE);
994 if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
997 if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) {
1000 if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) {
1003 if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV")) {
1010 * This method is called by SDL using JNI.
1012 public static boolean isTablet() {
1013 DisplayMetrics metrics = new DisplayMetrics();
1014 Activity activity = (Activity)getContext();
1015 if (activity == null) {
1018 activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
1020 double dWidthInches = metrics.widthPixels / (double)metrics.xdpi;
1021 double dHeightInches = metrics.heightPixels / (double)metrics.ydpi;
1023 double dDiagonal = Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches));
1025 // If our diagonal size is seven inches or greater, we consider ourselves a tablet.
1026 return (dDiagonal >= 7.0);
1030 * This method is called by SDL using JNI.
1032 public static boolean isChromebook() {
1033 if (getContext() == null) {
1036 return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
1040 * This method is called by SDL using JNI.
1042 public static boolean isDeXMode() {
1043 if (Build.VERSION.SDK_INT < 24) {
1047 final Configuration config = getContext().getResources().getConfiguration();
1048 final Class configClass = config.getClass();
1049 return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
1050 == configClass.getField("semDesktopModeEnabled").getInt(config);
1051 } catch(Exception ignored) {
1057 * This method is called by SDL using JNI.
1059 public static DisplayMetrics getDisplayDPI() {
1060 return getContext().getResources().getDisplayMetrics();
1064 * This method is called by SDL using JNI.
1066 public static boolean getManifestEnvironmentVariables() {
1068 ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA);
1069 Bundle bundle = applicationInfo.metaData;
1070 if (bundle == null) {
1073 String prefix = "SDL_ENV.";
1074 final int trimLength = prefix.length();
1075 for (String key : bundle.keySet()) {
1076 if (key.startsWith(prefix)) {
1077 String name = key.substring(trimLength);
1078 String value = bundle.get(key).toString();
1079 nativeSetenv(name, value);
1082 /* environment variables set! */
1084 } catch (Exception e) {
1085 Log.v("SDL", "exception " + e.toString());
1090 // This method is called by SDLControllerManager's API 26 Generic Motion Handler.
1091 public static View getContentView()
1093 return mSingleton.mLayout;
1096 static class ShowTextInputTask implements Runnable {
1098 * This is used to regulate the pan&scan method to have some offset from
1099 * the bottom edge of the input region and the top edge of an input
1100 * method (soft keyboard)
1102 static final int HEIGHT_PADDING = 15;
1104 public int x, y, w, h;
1106 public ShowTextInputTask(int x, int y, int w, int h) {
1112 /* Minimum size of 1 pixel, so it takes focus. */
1116 if (this.h + HEIGHT_PADDING <= 0) {
1117 this.h = 1 - HEIGHT_PADDING;
1123 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING);
1124 params.leftMargin = x;
1125 params.topMargin = y;
1127 if (mTextEdit == null) {
1128 mTextEdit = new DummyEdit(SDL.getContext());
1130 mLayout.addView(mTextEdit, params);
1132 mTextEdit.setLayoutParams(params);
1135 mTextEdit.setVisibility(View.VISIBLE);
1136 mTextEdit.requestFocus();
1138 InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1139 imm.showSoftInput(mTextEdit, 0);
1141 mScreenKeyboardShown = true;
1146 * This method is called by SDL using JNI.
1148 public static boolean showTextInput(int x, int y, int w, int h) {
1149 // Transfer the task to the main thread as a Runnable
1150 return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h));
1153 public static boolean isTextInputEvent(KeyEvent event) {
1155 // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT
1156 if (event.isCtrlPressed()) {
1160 return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE;
1164 * This method is called by SDL using JNI.
1166 public static Surface getNativeSurface() {
1167 if (SDLActivity.mSurface == null) {
1170 return SDLActivity.mSurface.getNativeSurface();
1174 * This method is called by SDL using JNI.
1176 public static void setSurfaceViewFormat(int format) {
1177 mSingleton.sendCommand(COMMAND_CHANGE_SURFACEVIEW_FORMAT, format);
1184 * This method is called by SDL using JNI.
1186 public static void initTouch() {
1187 int[] ids = InputDevice.getDeviceIds();
1189 for (int i = 0; i < ids.length; ++i) {
1190 InputDevice device = InputDevice.getDevice(ids[i]);
1191 if (device != null && (device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) != 0) {
1192 nativeAddTouch(device.getId(), device.getName());
1197 // APK expansion files support
1199 /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */
1200 private static Object expansionFile;
1202 /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */
1203 private static Method expansionFileMethod;
1206 * This method is called by SDL using JNI.
1207 * @return an InputStream on success or null if no expansion file was used.
1208 * @throws IOException on errors. Message is set for the SDL error message.
1210 public static InputStream openAPKExpansionInputStream(String fileName) throws IOException {
1211 // Get a ZipResourceFile representing a merger of both the main and patch files
1212 if (expansionFile == null) {
1213 String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION");
1214 if (mainHint == null) {
1215 return null; // no expansion use if no main version was set
1217 String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION");
1218 if (patchHint == null) {
1219 return null; // no expansion use if no patch version was set
1222 Integer mainVersion;
1223 Integer patchVersion;
1225 mainVersion = Integer.valueOf(mainHint);
1226 patchVersion = Integer.valueOf(patchHint);
1227 } catch (NumberFormatException ex) {
1228 ex.printStackTrace();
1229 throw new IOException("No valid file versions set for APK expansion files", ex);
1233 // To avoid direct dependency on Google APK expansion library that is
1234 // not a part of Android SDK we access it using reflection
1235 expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport")
1236 .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class)
1237 .invoke(null, SDL.getContext(), mainVersion, patchVersion);
1239 expansionFileMethod = expansionFile.getClass()
1240 .getMethod("getInputStream", String.class);
1241 } catch (Exception ex) {
1242 ex.printStackTrace();
1243 expansionFile = null;
1244 expansionFileMethod = null;
1245 throw new IOException("Could not access APK expansion support library", ex);
1249 // Get an input stream for a known file inside the expansion file ZIPs
1250 InputStream fileStream;
1252 fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName);
1253 } catch (Exception ex) {
1254 // calling "getInputStream" failed
1255 ex.printStackTrace();
1256 throw new IOException("Could not open stream from APK expansion file", ex);
1259 if (fileStream == null) {
1260 // calling "getInputStream" was successful but null was returned
1261 throw new IOException("Could not find path in APK expansion file");
1269 /** Result of current messagebox. Also used for blocking the calling thread. */
1270 protected final int[] messageboxSelection = new int[1];
1272 /** Id of current dialog. */
1273 protected int dialogs = 0;
1276 * This method is called by SDL using JNI.
1277 * Shows the messagebox from UI thread and block calling thread.
1278 * buttonFlags, buttonIds and buttonTexts must have same length.
1279 * @param buttonFlags array containing flags for every button.
1280 * @param buttonIds array containing id for every button.
1281 * @param buttonTexts array containing text for every button.
1282 * @param colors null for default or array of length 5 containing colors.
1283 * @return button id or -1.
1285 public int messageboxShowMessageBox(
1288 final String message,
1289 final int[] buttonFlags,
1290 final int[] buttonIds,
1291 final String[] buttonTexts,
1292 final int[] colors) {
1294 messageboxSelection[0] = -1;
1298 if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
1299 return -1; // implementation broken
1302 // collect arguments for Dialog
1304 final Bundle args = new Bundle();
1305 args.putInt("flags", flags);
1306 args.putString("title", title);
1307 args.putString("message", message);
1308 args.putIntArray("buttonFlags", buttonFlags);
1309 args.putIntArray("buttonIds", buttonIds);
1310 args.putStringArray("buttonTexts", buttonTexts);
1311 args.putIntArray("colors", colors);
1313 // trigger Dialog creation on UI thread
1315 runOnUiThread(new Runnable() {
1318 showDialog(dialogs++, args);
1322 // block the calling thread
1324 synchronized (messageboxSelection) {
1326 messageboxSelection.wait();
1327 } catch (InterruptedException ex) {
1328 ex.printStackTrace();
1333 // return selected value
1335 return messageboxSelection[0];
1339 protected Dialog onCreateDialog(int ignore, Bundle args) {
1341 // TODO set values from "flags" to messagebox dialog
1345 int[] colors = args.getIntArray("colors");
1346 int backgroundColor;
1348 int buttonBorderColor;
1349 int buttonBackgroundColor;
1350 int buttonSelectedColor;
1351 if (colors != null) {
1353 backgroundColor = colors[++i];
1354 textColor = colors[++i];
1355 buttonBorderColor = colors[++i];
1356 buttonBackgroundColor = colors[++i];
1357 buttonSelectedColor = colors[++i];
1359 backgroundColor = Color.TRANSPARENT;
1360 textColor = Color.TRANSPARENT;
1361 buttonBorderColor = Color.TRANSPARENT;
1362 buttonBackgroundColor = Color.TRANSPARENT;
1363 buttonSelectedColor = Color.TRANSPARENT;
1366 // create dialog with title and a listener to wake up calling thread
1368 final Dialog dialog = new Dialog(this);
1369 dialog.setTitle(args.getString("title"));
1370 dialog.setCancelable(false);
1371 dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
1373 public void onDismiss(DialogInterface unused) {
1374 synchronized (messageboxSelection) {
1375 messageboxSelection.notify();
1382 TextView message = new TextView(this);
1383 message.setGravity(Gravity.CENTER);
1384 message.setText(args.getString("message"));
1385 if (textColor != Color.TRANSPARENT) {
1386 message.setTextColor(textColor);
1391 int[] buttonFlags = args.getIntArray("buttonFlags");
1392 int[] buttonIds = args.getIntArray("buttonIds");
1393 String[] buttonTexts = args.getStringArray("buttonTexts");
1395 final SparseArray<Button> mapping = new SparseArray<Button>();
1397 LinearLayout buttons = new LinearLayout(this);
1398 buttons.setOrientation(LinearLayout.HORIZONTAL);
1399 buttons.setGravity(Gravity.CENTER);
1400 for (int i = 0; i < buttonTexts.length; ++i) {
1401 Button button = new Button(this);
1402 final int id = buttonIds[i];
1403 button.setOnClickListener(new View.OnClickListener() {
1405 public void onClick(View v) {
1406 messageboxSelection[0] = id;
1410 if (buttonFlags[i] != 0) {
1411 // see SDL_messagebox.h
1412 if ((buttonFlags[i] & 0x00000001) != 0) {
1413 mapping.put(KeyEvent.KEYCODE_ENTER, button);
1415 if ((buttonFlags[i] & 0x00000002) != 0) {
1416 mapping.put(KeyEvent.KEYCODE_ESCAPE, button); /* API 11 */
1419 button.setText(buttonTexts[i]);
1420 if (textColor != Color.TRANSPARENT) {
1421 button.setTextColor(textColor);
1423 if (buttonBorderColor != Color.TRANSPARENT) {
1424 // TODO set color for border of messagebox button
1426 if (buttonBackgroundColor != Color.TRANSPARENT) {
1427 Drawable drawable = button.getBackground();
1428 if (drawable == null) {
1429 // setting the color this way removes the style
1430 button.setBackgroundColor(buttonBackgroundColor);
1432 // setting the color this way keeps the style (gradient, padding, etc.)
1433 drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
1436 if (buttonSelectedColor != Color.TRANSPARENT) {
1437 // TODO set color for selected messagebox button
1439 buttons.addView(button);
1444 LinearLayout content = new LinearLayout(this);
1445 content.setOrientation(LinearLayout.VERTICAL);
1446 content.addView(message);
1447 content.addView(buttons);
1448 if (backgroundColor != Color.TRANSPARENT) {
1449 content.setBackgroundColor(backgroundColor);
1452 // add content to dialog and return
1454 dialog.setContentView(content);
1455 dialog.setOnKeyListener(new Dialog.OnKeyListener() {
1457 public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) {
1458 Button button = mapping.get(keyCode);
1459 if (button != null) {
1460 if (event.getAction() == KeyEvent.ACTION_UP) {
1461 button.performClick();
1463 return true; // also for ignored actions
1472 private final Runnable rehideSystemUi = new Runnable() {
1475 int flags = View.SYSTEM_UI_FLAG_FULLSCREEN |
1476 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
1477 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
1478 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
1479 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
1480 View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE;
1482 SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags);
1486 public void onSystemUiVisibilityChange(int visibility) {
1487 if (SDLActivity.mFullscreenModeActive && ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)) {
1489 Handler handler = getWindow().getDecorView().getHandler();
1490 if (handler != null) {
1491 handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop.
1492 handler.postDelayed(rehideSystemUi, 2000);
1499 * This method is called by SDL using JNI.
1501 public static boolean clipboardHasText() {
1502 return mClipboardHandler.clipboardHasText();
1506 * This method is called by SDL using JNI.
1508 public static String clipboardGetText() {
1509 return mClipboardHandler.clipboardGetText();
1513 * This method is called by SDL using JNI.
1515 public static void clipboardSetText(String string) {
1516 mClipboardHandler.clipboardSetText(string);
1520 * This method is called by SDL using JNI.
1522 public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) {
1523 Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
1526 if (Build.VERSION.SDK_INT >= 24) {
1528 mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY));
1529 } catch (Exception e) {
1535 return mLastCursorID;
1539 * This method is called by SDL using JNI.
1541 public static boolean setCustomCursor(int cursorID) {
1543 if (Build.VERSION.SDK_INT >= 24) {
1545 mSurface.setPointerIcon(mCursors.get(cursorID));
1546 } catch (Exception e) {
1556 * This method is called by SDL using JNI.
1558 public static boolean setSystemCursor(int cursorID) {
1559 int cursor_type = 0; //PointerIcon.TYPE_NULL;
1561 case SDL_SYSTEM_CURSOR_ARROW:
1562 cursor_type = 1000; //PointerIcon.TYPE_ARROW;
1564 case SDL_SYSTEM_CURSOR_IBEAM:
1565 cursor_type = 1008; //PointerIcon.TYPE_TEXT;
1567 case SDL_SYSTEM_CURSOR_WAIT:
1568 cursor_type = 1004; //PointerIcon.TYPE_WAIT;
1570 case SDL_SYSTEM_CURSOR_CROSSHAIR:
1571 cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR;
1573 case SDL_SYSTEM_CURSOR_WAITARROW:
1574 cursor_type = 1004; //PointerIcon.TYPE_WAIT;
1576 case SDL_SYSTEM_CURSOR_SIZENWSE:
1577 cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
1579 case SDL_SYSTEM_CURSOR_SIZENESW:
1580 cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
1582 case SDL_SYSTEM_CURSOR_SIZEWE:
1583 cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
1585 case SDL_SYSTEM_CURSOR_SIZENS:
1586 cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
1588 case SDL_SYSTEM_CURSOR_SIZEALL:
1589 cursor_type = 1020; //PointerIcon.TYPE_GRAB;
1591 case SDL_SYSTEM_CURSOR_NO:
1592 cursor_type = 1012; //PointerIcon.TYPE_NO_DROP;
1594 case SDL_SYSTEM_CURSOR_HAND:
1595 cursor_type = 1002; //PointerIcon.TYPE_HAND;
1598 if (Build.VERSION.SDK_INT >= 24) {
1600 mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type));
1601 } catch (Exception e) {
1609 * This method is called by SDL using JNI.
1611 public static void requestPermission(String permission, int requestCode) {
1612 if (Build.VERSION.SDK_INT < 23) {
1613 nativePermissionResult(requestCode, true);
1617 Activity activity = (Activity)getContext();
1618 if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
1619 activity.requestPermissions(new String[]{permission}, requestCode);
1621 nativePermissionResult(requestCode, true);
1626 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
1627 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
1628 nativePermissionResult(requestCode, true);
1630 nativePermissionResult(requestCode, false);
1636 Simple runnable to start the SDL application
1638 class SDLMain implements Runnable {
1642 String library = SDLActivity.mSingleton.getMainSharedObject();
1643 String function = SDLActivity.mSingleton.getMainFunction();
1644 String[] arguments = SDLActivity.mSingleton.getArguments();
1647 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY);
1648 } catch (Exception e) {
1649 Log.v("SDL", "modify thread properties failed " + e.toString());
1652 Log.v("SDL", "Running main function " + function + " from library " + library);
1654 SDLActivity.nativeRunMain(library, function, arguments);
1656 Log.v("SDL", "Finished main function");
1658 if (SDLActivity.mSingleton == null || SDLActivity.mSingleton.isFinishing()) {
1659 // Activity is already being destroyed
1661 // Let's finish the Activity
1662 SDLActivity.mSDLThread = null;
1663 SDLActivity.mSingleton.finish();
1670 SDLSurface. This is what we draw on, so we need to know when it's created
1671 in order to do anything useful.
1673 Because of this, that's where we set up the SDL thread
1675 class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
1676 View.OnKeyListener, View.OnTouchListener, SensorEventListener {
1679 protected SensorManager mSensorManager;
1680 protected Display mDisplay;
1682 // Keep track of the surface size to normalize touch events
1683 protected float mWidth, mHeight;
1685 // Is SurfaceView ready for rendering
1686 public boolean mIsSurfaceReady;
1689 public SDLSurface(Context context) {
1691 getHolder().addCallback(this);
1694 setFocusableInTouchMode(true);
1696 setOnKeyListener(this);
1697 setOnTouchListener(this);
1699 mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
1700 mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
1702 setOnGenericMotionListener(SDLActivity.getMotionListener());
1704 // Some arbitrary defaults to avoid a potential division by zero
1708 mIsSurfaceReady = false;
1711 public void handlePause() {
1712 enableSensor(Sensor.TYPE_ACCELEROMETER, false);
1715 public void handleResume() {
1717 setFocusableInTouchMode(true);
1719 setOnKeyListener(this);
1720 setOnTouchListener(this);
1721 enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1724 public Surface getNativeSurface() {
1725 return getHolder().getSurface();
1728 // Called when we have a valid drawing surface
1730 public void surfaceCreated(SurfaceHolder holder) {
1731 Log.v("SDL", "surfaceCreated()");
1732 SDLActivity.onNativeSurfaceCreated();
1735 // Called when we lose the surface
1737 public void surfaceDestroyed(SurfaceHolder holder) {
1738 Log.v("SDL", "surfaceDestroyed()");
1740 // Transition to pause, if needed
1741 SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
1742 SDLActivity.handleNativeState();
1744 mIsSurfaceReady = false;
1745 SDLActivity.onNativeSurfaceDestroyed();
1748 // Called when the surface is resized
1750 public void surfaceChanged(SurfaceHolder holder,
1751 int format, int width, int height) {
1752 Log.v("SDL", "surfaceChanged()");
1754 if (SDLActivity.mSingleton == null) {
1758 int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
1760 case PixelFormat.RGBA_8888:
1761 Log.v("SDL", "pixel format RGBA_8888");
1762 sdlFormat = 0x16462004; // SDL_PIXELFORMAT_RGBA8888
1764 case PixelFormat.RGBX_8888:
1765 Log.v("SDL", "pixel format RGBX_8888");
1766 sdlFormat = 0x16261804; // SDL_PIXELFORMAT_RGBX8888
1768 case PixelFormat.RGB_565:
1769 Log.v("SDL", "pixel format RGB_565");
1770 sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565
1772 case PixelFormat.RGB_888:
1773 Log.v("SDL", "pixel format RGB_888");
1774 // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead?
1775 sdlFormat = 0x16161804; // SDL_PIXELFORMAT_RGB888
1778 Log.v("SDL", "pixel format unknown " + format);
1784 int nDeviceWidth = width;
1785 int nDeviceHeight = height;
1788 if (Build.VERSION.SDK_INT >= 17) {
1789 android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics();
1790 mDisplay.getRealMetrics( realMetrics );
1791 nDeviceWidth = realMetrics.widthPixels;
1792 nDeviceHeight = realMetrics.heightPixels;
1795 catch ( java.lang.Throwable throwable ) {}
1797 synchronized(SDLActivity.getContext()) {
1798 // In case we're waiting on a size change after going fullscreen, send a notification.
1799 SDLActivity.getContext().notifyAll();
1802 Log.v("SDL", "Window size: " + width + "x" + height);
1803 Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
1804 SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, sdlFormat, mDisplay.getRefreshRate());
1805 SDLActivity.onNativeResize();
1807 // Prevent a screen distortion glitch,
1808 // for instance when the device is in Landscape and a Portrait App is resumed.
1809 boolean skip = false;
1810 int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
1812 if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
1816 else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT)
1818 if (mWidth > mHeight) {
1821 } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
1822 if (mWidth < mHeight) {
1827 // Special Patch for Square Resolution: Black Berry Passport
1829 double min = Math.min(mWidth, mHeight);
1830 double max = Math.max(mWidth, mHeight);
1832 if (max / min < 1.20) {
1833 Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
1838 // Don't skip in MultiWindow.
1840 if (Build.VERSION.SDK_INT >= 24) {
1841 if (SDLActivity.mSingleton.isInMultiWindowMode()) {
1842 Log.v("SDL", "Don't skip in Multi-Window");
1849 Log.v("SDL", "Skip .. Surface is not ready.");
1850 mIsSurfaceReady = false;
1854 /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
1855 SDLActivity.onNativeSurfaceChanged();
1857 /* Surface is ready */
1858 mIsSurfaceReady = true;
1860 SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
1861 SDLActivity.handleNativeState();
1866 public boolean onKey(View v, int keyCode, KeyEvent event) {
1868 int deviceId = event.getDeviceId();
1869 int source = event.getSource();
1871 // Dispatch the different events depending on where they come from
1872 // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
1873 // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
1875 // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
1876 // SOURCE_JOYSTICK, while its key events arrive from the keyboard source
1877 // So, retrieve the device itself and check all of its sources
1878 if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) {
1879 // Note that we process events with specific key codes here
1880 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1881 if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) {
1884 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1885 if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) {
1891 if (source == InputDevice.SOURCE_UNKNOWN) {
1892 InputDevice device = InputDevice.getDevice(deviceId);
1893 if (device != null) {
1894 source = device.getSources();
1898 if ((source & InputDevice.SOURCE_KEYBOARD) != 0) {
1899 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1900 //Log.v("SDL", "key down: " + keyCode);
1901 if (SDLActivity.isTextInputEvent(event)) {
1902 SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
1904 SDLActivity.onNativeKeyDown(keyCode);
1907 else if (event.getAction() == KeyEvent.ACTION_UP) {
1908 //Log.v("SDL", "key up: " + keyCode);
1909 SDLActivity.onNativeKeyUp(keyCode);
1914 if ((source & InputDevice.SOURCE_MOUSE) != 0) {
1915 // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
1916 // they are ignored here because sending them as mouse input to SDL is messy
1917 if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
1918 switch (event.getAction()) {
1919 case KeyEvent.ACTION_DOWN:
1920 case KeyEvent.ACTION_UP:
1921 // mark the event as handled or it will be handled by system
1922 // handling KEYCODE_BACK by system will call onBackPressed()
1933 public boolean onTouch(View v, MotionEvent event) {
1934 /* Ref: http://developer.android.com/training/gestures/multi.html */
1935 final int touchDevId = event.getDeviceId();
1936 final int pointerCount = event.getPointerCount();
1937 int action = event.getActionMasked();
1938 int pointerFingerId;
1943 // 12290 = Samsung DeX mode desktop mouse
1944 // 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
1945 // 0x2 = SOURCE_CLASS_POINTER
1946 if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
1948 mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event);
1949 } catch(Exception e) {
1950 mouseButton = 1; // oh well.
1953 // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
1954 // if we are. We'll leverage our existing mouse motion listener
1955 SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
1956 x = motionListener.getEventX(event);
1957 y = motionListener.getEventY(event);
1959 SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
1962 case MotionEvent.ACTION_MOVE:
1963 for (i = 0; i < pointerCount; i++) {
1964 pointerFingerId = event.getPointerId(i);
1965 x = event.getX(i) / mWidth;
1966 y = event.getY(i) / mHeight;
1967 p = event.getPressure(i);
1969 // may be larger than 1.0f on some devices
1970 // see the documentation of getPressure(i)
1973 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1977 case MotionEvent.ACTION_UP:
1978 case MotionEvent.ACTION_DOWN:
1979 // Primary pointer up/down, the index is always zero
1981 case MotionEvent.ACTION_POINTER_UP:
1982 case MotionEvent.ACTION_POINTER_DOWN:
1983 // Non primary pointer up/down
1985 i = event.getActionIndex();
1988 pointerFingerId = event.getPointerId(i);
1989 x = event.getX(i) / mWidth;
1990 y = event.getY(i) / mHeight;
1991 p = event.getPressure(i);
1993 // may be larger than 1.0f on some devices
1994 // see the documentation of getPressure(i)
1997 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
2000 case MotionEvent.ACTION_CANCEL:
2001 for (i = 0; i < pointerCount; i++) {
2002 pointerFingerId = event.getPointerId(i);
2003 x = event.getX(i) / mWidth;
2004 y = event.getY(i) / mHeight;
2005 p = event.getPressure(i);
2007 // may be larger than 1.0f on some devices
2008 // see the documentation of getPressure(i)
2011 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
2024 public void enableSensor(int sensortype, boolean enabled) {
2025 // TODO: This uses getDefaultSensor - what if we have >1 accels?
2027 mSensorManager.registerListener(this,
2028 mSensorManager.getDefaultSensor(sensortype),
2029 SensorManager.SENSOR_DELAY_GAME, null);
2031 mSensorManager.unregisterListener(this,
2032 mSensorManager.getDefaultSensor(sensortype));
2037 public void onAccuracyChanged(Sensor sensor, int accuracy) {
2042 public void onSensorChanged(SensorEvent event) {
2043 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
2045 // Since we may have an orientation set, we won't receive onConfigurationChanged events.
2046 // We thus should check here.
2047 int newOrientation = SDLActivity.SDL_ORIENTATION_UNKNOWN;
2050 switch (mDisplay.getRotation()) {
2051 case Surface.ROTATION_90:
2052 x = -event.values[1];
2053 y = event.values[0];
2054 newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
2056 case Surface.ROTATION_270:
2057 x = event.values[1];
2058 y = -event.values[0];
2059 newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
2061 case Surface.ROTATION_180:
2062 x = -event.values[0];
2063 y = -event.values[1];
2064 newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
2067 x = event.values[0];
2068 y = event.values[1];
2069 newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
2073 if (newOrientation != SDLActivity.mCurrentOrientation) {
2074 SDLActivity.mCurrentOrientation = newOrientation;
2075 SDLActivity.onNativeOrientationChanged(newOrientation);
2078 SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
2079 y / SensorManager.GRAVITY_EARTH,
2080 event.values[2] / SensorManager.GRAVITY_EARTH);
2086 // Captured pointer events for API 26.
2087 public boolean onCapturedPointerEvent(MotionEvent event)
2089 int action = event.getActionMasked();
2093 case MotionEvent.ACTION_SCROLL:
2094 x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
2095 y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
2096 SDLActivity.onNativeMouse(0, action, x, y, false);
2099 case MotionEvent.ACTION_HOVER_MOVE:
2100 case MotionEvent.ACTION_MOVE:
2103 SDLActivity.onNativeMouse(0, action, x, y, true);
2106 case MotionEvent.ACTION_BUTTON_PRESS:
2107 case MotionEvent.ACTION_BUTTON_RELEASE:
2109 // Change our action value to what SDL's code expects.
2110 if (action == MotionEvent.ACTION_BUTTON_PRESS) {
2111 action = MotionEvent.ACTION_DOWN;
2113 else if (action == MotionEvent.ACTION_BUTTON_RELEASE) {
2114 action = MotionEvent.ACTION_UP;
2119 int button = event.getButtonState();
2121 SDLActivity.onNativeMouse(button, action, x, y, true);
2130 /* This is a fake invisible editor view that receives the input and defines the
2133 class DummyEdit extends View implements View.OnKeyListener {
2136 public DummyEdit(Context context) {
2138 setFocusableInTouchMode(true);
2140 setOnKeyListener(this);
2144 public boolean onCheckIsTextEditor() {
2149 public boolean onKey(View v, int keyCode, KeyEvent event) {
2151 * This handles the hardware keyboard input
2153 if (event.getAction() == KeyEvent.ACTION_DOWN) {
2154 if (SDLActivity.isTextInputEvent(event)) {
2155 ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
2158 SDLActivity.onNativeKeyDown(keyCode);
2160 } else if (event.getAction() == KeyEvent.ACTION_UP) {
2161 SDLActivity.onNativeKeyUp(keyCode);
2169 public boolean onKeyPreIme (int keyCode, KeyEvent event) {
2170 // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
2171 // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
2172 // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
2173 // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
2174 // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
2175 // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
2176 if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
2177 if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
2178 SDLActivity.onNativeKeyboardFocusLost();
2181 return super.onKeyPreIme(keyCode, event);
2185 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
2186 ic = new SDLInputConnection(this, true);
2188 outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
2189 outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
2190 | EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
2196 class SDLInputConnection extends BaseInputConnection {
2198 public SDLInputConnection(View targetView, boolean fullEditor) {
2199 super(targetView, fullEditor);
2204 public boolean sendKeyEvent(KeyEvent event) {
2206 * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
2207 * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
2208 * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
2209 * that still do, we empty this out.
2213 * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
2214 * as we do with physical keyboards, let's just use it to hide the keyboard.
2217 if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
2218 if (SDLActivity.onNativeSoftReturnKey()) {
2224 return super.sendKeyEvent(event);
2228 public boolean commitText(CharSequence text, int newCursorPosition) {
2230 for (int i = 0; i < text.length(); i++) {
2231 char c = text.charAt(i);
2233 if (SDLActivity.onNativeSoftReturnKey()) {
2237 nativeGenerateScancodeForUnichar(c);
2240 SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition);
2242 return super.commitText(text, newCursorPosition);
2246 public boolean setComposingText(CharSequence text, int newCursorPosition) {
2248 nativeSetComposingText(text.toString(), newCursorPosition);
2250 return super.setComposingText(text, newCursorPosition);
2253 public static native void nativeCommitText(String text, int newCursorPosition);
2255 public native void nativeGenerateScancodeForUnichar(char c);
2257 public native void nativeSetComposingText(String text, int newCursorPosition);
2260 public boolean deleteSurroundingText(int beforeLength, int afterLength) {
2261 // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
2262 // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
2263 if (beforeLength > 0 && afterLength == 0) {
2266 while (beforeLength-- > 0) {
2267 boolean ret_key = sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
2268 && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
2269 ret = ret && ret_key;
2274 return super.deleteSurroundingText(beforeLength, afterLength);
2278 interface SDLClipboardHandler {
2280 public boolean clipboardHasText();
2281 public String clipboardGetText();
2282 public void clipboardSetText(String string);
2287 class SDLClipboardHandler_API11 implements
2288 SDLClipboardHandler,
2289 android.content.ClipboardManager.OnPrimaryClipChangedListener {
2291 protected android.content.ClipboardManager mClipMgr;
2293 SDLClipboardHandler_API11() {
2294 mClipMgr = (android.content.ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
2295 mClipMgr.addPrimaryClipChangedListener(this);
2299 public boolean clipboardHasText() {
2300 return mClipMgr.hasText();
2304 public String clipboardGetText() {
2306 text = mClipMgr.getText();
2308 return text.toString();
2314 public void clipboardSetText(String string) {
2315 mClipMgr.removePrimaryClipChangedListener(this);
2316 mClipMgr.setText(string);
2317 mClipMgr.addPrimaryClipChangedListener(this);
2321 public void onPrimaryClipChanged() {
2322 SDLActivity.onNativeClipboardChanged();