1 package org.libsdl.app;
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;
57 import java.util.Hashtable;
58 import java.util.Locale;
64 public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
65 private static final String TAG = "SDL";
67 public static boolean mIsResumedCalled, mHasFocus;
68 public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24);
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;
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;
91 protected static int mCurrentOrientation;
92 protected static Locale mCurrentLocale;
94 // Handle the state of the native layer
95 public enum NativeState {
99 public static NativeState mNextNativeState;
100 public static NativeState mCurrentNativeState;
102 /** If shared libraries (e.g. SDL or the native application) could not be loaded. */
103 public static boolean mBrokenLibraries = true;
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;
117 // This is what SDL runs in. It invokes SDL_main(), eventually
118 protected static Thread mSDLThread;
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();
127 mMotionListener = new SDLGenericMotionListener_API12();
131 return mMotionListener;
135 * This method returns the name of the shared object with the application entry point
136 * It can be overridden by derived classes.
138 protected String getMainSharedObject() {
140 String[] libraries = SDLActivity.mSingleton.getLibraries();
141 if (libraries.length > 0) {
142 library = "lib" + libraries[libraries.length - 1] + ".so";
144 library = "libmain.so";
146 return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
150 * This method returns the name of the application entry point
151 * It can be overridden by derived classes.
153 protected String getMainFunction() {
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").
165 protected String[] getLibraries() {
166 return new String[] {
177 public void loadLibraries() {
178 for (String lib : getLibraries()) {
179 SDL.loadLibrary(lib);
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.
189 protected String[] getArguments() {
190 return new String[0];
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
200 mClipboardHandler = null;
201 mCursors = new Hashtable<Integer, PointerIcon>();
204 mIsResumedCalled = false;
206 mNextNativeState = NativeState.INIT;
207 mCurrentNativeState = NativeState.INIT;
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);
219 Thread.currentThread().setName("SDLActivity");
220 } catch (Exception e) {
221 Log.v(TAG, "modify thread properties failed " + e.toString());
224 // Load shared libraries
225 String errorMsgBrokenLib = "";
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();
239 if (mBrokenLibraries)
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() {
251 public void onClick(DialogInterface dialog,int id) {
252 // if this button is clicked, close current activity
253 SDLActivity.mSingleton.finish();
256 dlgAlert.setCancelable(false);
257 dlgAlert.create().show();
268 // So we can call stuff from static callbacks
270 SDL.setContext(this);
272 mClipboardHandler = new SDLClipboardHandler();
274 mHIDDeviceManager = HIDDeviceManager.acquire(this);
276 // Set up the surface
277 mSurface = new SDLSurface(getApplication());
279 mLayout = new RelativeLayout(this);
280 mLayout.addView(mSurface);
282 // Get our current screen orientation and pass it down.
283 mCurrentOrientation = SDLActivity.getCurrentOrientation();
284 // Only record current orientation
285 SDLActivity.onNativeOrientationChanged(mCurrentOrientation);
288 if (Build.VERSION.SDK_INT < 24) {
289 mCurrentLocale = getContext().getResources().getConfiguration().locale;
291 mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0);
293 } catch(Exception ignored) {
296 setContentView(mLayout);
298 setWindowStyle(false);
300 getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
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);
313 protected void pauseNativeThread() {
314 mNextNativeState = NativeState.PAUSED;
315 mIsResumedCalled = false;
317 if (SDLActivity.mBrokenLibraries) {
321 SDLActivity.handleNativeState();
324 protected void resumeNativeThread() {
325 mNextNativeState = NativeState.RESUMED;
326 mIsResumedCalled = true;
328 if (SDLActivity.mBrokenLibraries) {
332 SDLActivity.handleNativeState();
337 protected void onPause() {
338 Log.v(TAG, "onPause()");
341 if (mHIDDeviceManager != null) {
342 mHIDDeviceManager.setFrozen(true);
344 if (!mHasMultiWindow) {
350 protected void onResume() {
351 Log.v(TAG, "onResume()");
354 if (mHIDDeviceManager != null) {
355 mHIDDeviceManager.setFrozen(false);
357 if (!mHasMultiWindow) {
358 resumeNativeThread();
363 protected void onStop() {
364 Log.v(TAG, "onStop()");
366 if (mHasMultiWindow) {
372 protected void onStart() {
373 Log.v(TAG, "onStart()");
375 if (mHasMultiWindow) {
376 resumeNativeThread();
380 public static int getCurrentOrientation() {
381 int result = SDL_ORIENTATION_UNKNOWN;
383 Activity activity = (Activity)getContext();
384 if (activity == null) {
387 Display display = activity.getWindowManager().getDefaultDisplay();
389 switch (display.getRotation()) {
390 case Surface.ROTATION_0:
391 result = SDL_ORIENTATION_PORTRAIT;
394 case Surface.ROTATION_90:
395 result = SDL_ORIENTATION_LANDSCAPE;
398 case Surface.ROTATION_180:
399 result = SDL_ORIENTATION_PORTRAIT_FLIPPED;
402 case Surface.ROTATION_270:
403 result = SDL_ORIENTATION_LANDSCAPE_FLIPPED;
411 public void onWindowFocusChanged(boolean hasFocus) {
412 super.onWindowFocusChanged(hasFocus);
413 Log.v(TAG, "onWindowFocusChanged(): " + hasFocus);
415 if (SDLActivity.mBrokenLibraries) {
419 mHasFocus = hasFocus;
421 mNextNativeState = NativeState.RESUMED;
422 SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded();
424 SDLActivity.handleNativeState();
425 nativeFocusChanged(true);
428 nativeFocusChanged(false);
429 if (!mHasMultiWindow) {
430 mNextNativeState = NativeState.PAUSED;
431 SDLActivity.handleNativeState();
437 public void onLowMemory() {
438 Log.v(TAG, "onLowMemory()");
441 if (SDLActivity.mBrokenLibraries) {
445 SDLActivity.nativeLowMemory();
449 public void onConfigurationChanged(Configuration newConfig) {
450 Log.v(TAG, "onConfigurationChanged()");
451 super.onConfigurationChanged(newConfig);
453 if (SDLActivity.mBrokenLibraries) {
457 if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) {
458 mCurrentLocale = newConfig.locale;
459 SDLActivity.onNativeLocaleChanged();
464 protected void onDestroy() {
465 Log.v(TAG, "onDestroy()");
467 if (mHIDDeviceManager != null) {
468 HIDDeviceManager.release(mHIDDeviceManager);
469 mHIDDeviceManager = null;
472 if (SDLActivity.mBrokenLibraries) {
477 if (SDLActivity.mSDLThread != null) {
479 // Send Quit event to "SDLThread" thread
480 SDLActivity.nativeSendQuit();
482 // Wait for "SDLThread" thread to end
484 SDLActivity.mSDLThread.join();
485 } catch(Exception e) {
486 Log.v(TAG, "Problem stopping SDLThread: " + e);
490 SDLActivity.nativeQuit();
496 public void onBackPressed() {
497 // Check if we want to block the back button in case of mouse right click.
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.
502 boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false);
504 // Exit and let the mouse handler handle this button (if appropriate)
508 // Default system back button behavior.
509 if (!isFinishing()) {
510 super.onBackPressed();
514 // Called by JNI from SDL.
515 public static void manualBackButton() {
516 mSingleton.pressBackButton();
519 // Used to get us onto the activity's main thread
520 public void pressBackButton() {
521 runOnUiThread(new Runnable() {
524 if (!SDLActivity.this.isFinishing()) {
525 SDLActivity.this.superOnBackPressed();
531 // Used to access the system back behavior.
532 public void superOnBackPressed() {
533 super.onBackPressed();
537 public boolean dispatchKeyEvent(KeyEvent event) {
539 if (SDLActivity.mBrokenLibraries) {
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 */
553 return super.dispatchKeyEvent(event);
556 /* Transition to next state */
557 public static void handleNativeState() {
559 if (mNextNativeState == mCurrentNativeState) {
560 // Already in same state, discard.
564 // Try a transition to init state
565 if (mNextNativeState == NativeState.INIT) {
567 mCurrentNativeState = mNextNativeState;
571 // Try a transition to paused state
572 if (mNextNativeState == NativeState.PAUSED) {
573 if (mSDLThread != null) {
576 if (mSurface != null) {
577 mSurface.handlePause();
579 mCurrentNativeState = mNextNativeState;
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?
591 mSDLThread = new Thread(new SDLMain(), "SDLThread");
592 mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true);
595 // No nativeResume(), don't signal Android_ResumeSem
599 mSurface.handleResume();
601 mCurrentNativeState = mNextNativeState;
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;
612 protected static final int COMMAND_USER = 0x8000;
614 protected static boolean mFullscreenModeActive;
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.
624 protected boolean onUnhandledMessage(int command, Object param) {
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.
633 protected static class SDLCommandHandler extends Handler {
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");
642 case COMMAND_CHANGE_TITLE:
643 if (context instanceof Activity) {
644 ((Activity) context).setTitle((String)msg.obj);
646 Log.e(TAG, "error handling message, getContext() returned no Activity");
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;
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;
674 Log.e(TAG, "error handling message, getContext() returned no Activity");
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));
685 InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
686 imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);
688 mScreenKeyboardShown = false;
690 mSurface.requestFocus();
693 case COMMAND_SET_KEEP_SCREEN_ON:
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);
701 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
708 if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) {
709 Log.e(TAG, "error handling message, command is " + msg.arg1);
715 // Handler for the messages
716 Handler commandHandler = new SDLCommandHandler();
718 // Send a message from the SDLMain thread
719 boolean sendCommand(int command, Object data) {
720 Message msg = commandHandler.obtainMessage();
723 boolean result = commandHandler.sendMessage(msg);
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.
730 boolean bShouldWait = false;
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);
738 boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) &&
739 (realMetrics.heightPixels == mSurface.getHeight()));
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;
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;
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.
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.
766 synchronized (SDLActivity.getContext()) {
768 SDLActivity.getContext().wait(500);
769 } catch (InterruptedException ie) {
770 ie.printStackTrace();
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,
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();
814 * This method is called by SDL using JNI.
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);
822 * This method is called by SDL using JNI.
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);
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
834 public static void setOrientation(int w, int h, boolean resizable, String hint)
836 if (mSingleton != null) {
837 mSingleton.setOrientationBis(w, h, resizable, hint);
842 * This can be overridden
844 public void setOrientationBis(int w, int h, boolean resizable, String hint)
846 int orientation_landscape = -1;
847 int orientation_portrait = -1;
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;
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;
866 boolean is_landscape_allowed = (orientation_landscape != -1);
867 boolean is_portrait_allowed = (orientation_portrait != -1);
868 int req; /* Requested orientation */
870 /* No valid hint, nothing is explicitly allowed */
871 if (!is_portrait_allowed && !is_landscape_allowed) {
873 /* All orientations are allowed */
874 req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
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);
880 /* At least one orientation is allowed */
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;
886 /* Use the only one allowed "orientation" */
887 req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
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);
894 /* Use the only one allowed "orientation" */
895 req = (is_landscape_allowed ? orientation_landscape : orientation_portrait);
900 Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint);
901 mSingleton.setRequestedOrientation(req);
905 * This method is called by SDL using JNI.
907 public static void minimizeWindow() {
909 if (mSingleton == null) {
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);
920 * This method is called by SDL using JNI.
922 public static boolean shouldMinimizeOnFocusLoss() {
924 if (Build.VERSION.SDK_INT >= 24) {
925 if (mSingleton == null) {
929 if (mSingleton.isInMultiWindowMode()) {
933 if (mSingleton.isInPictureInPictureMode()) {
944 * This method is called by SDL using JNI.
946 public static boolean isScreenKeyboardShown()
948 if (mTextEdit == null) {
952 if (!mScreenKeyboardShown) {
956 InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
957 return imm.isAcceptingText();
962 * This method is called by SDL using JNI.
964 public static boolean supportsRelativeMouse()
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.
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.
973 if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) {
977 return SDLActivity.getMotionListener().supportsRelativeMouse();
981 * This method is called by SDL using JNI.
983 public static boolean setRelativeMouseEnabled(boolean enabled)
985 if (enabled && !supportsRelativeMouse()) {
989 return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled);
993 * This method is called by SDL using JNI.
995 public static boolean sendMessage(int command, int param) {
996 if (mSingleton == null) {
999 return mSingleton.sendCommand(command, param);
1003 * This method is called by SDL using JNI.
1005 public static Context getContext() {
1006 return SDL.getContext();
1010 * This method is called by SDL using JNI.
1012 public static boolean isAndroidTV() {
1013 UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE);
1014 if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
1017 if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) {
1020 if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) {
1023 return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV");
1026 public static double getDiagonal()
1028 DisplayMetrics metrics = new DisplayMetrics();
1029 Activity activity = (Activity)getContext();
1030 if (activity == null) {
1033 activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
1035 double dWidthInches = metrics.widthPixels / (double)metrics.xdpi;
1036 double dHeightInches = metrics.heightPixels / (double)metrics.ydpi;
1038 return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches));
1042 * This method is called by SDL using JNI.
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);
1050 * This method is called by SDL using JNI.
1052 public static boolean isChromebook() {
1053 if (getContext() == null) {
1056 return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
1060 * This method is called by SDL using JNI.
1062 public static boolean isDeXMode() {
1063 if (Build.VERSION.SDK_INT < 24) {
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) {
1077 * This method is called by SDL using JNI.
1079 public static DisplayMetrics getDisplayDPI() {
1080 return getContext().getResources().getDisplayMetrics();
1084 * This method is called by SDL using JNI.
1086 public static boolean getManifestEnvironmentVariables() {
1088 if (getContext() == null) {
1092 ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA);
1093 Bundle bundle = applicationInfo.metaData;
1094 if (bundle == null) {
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);
1106 /* environment variables set! */
1108 } catch (Exception e) {
1109 Log.v(TAG, "exception " + e.toString());
1114 // This method is called by SDLControllerManager's API 26 Generic Motion Handler.
1115 public static View getContentView()
1120 static class ShowTextInputTask implements Runnable {
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)
1126 static final int HEIGHT_PADDING = 15;
1128 public int x, y, w, h;
1130 public ShowTextInputTask(int x, int y, int w, int h) {
1136 /* Minimum size of 1 pixel, so it takes focus. */
1140 if (this.h + HEIGHT_PADDING <= 0) {
1141 this.h = 1 - HEIGHT_PADDING;
1147 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING);
1148 params.leftMargin = x;
1149 params.topMargin = y;
1151 if (mTextEdit == null) {
1152 mTextEdit = new DummyEdit(SDL.getContext());
1154 mLayout.addView(mTextEdit, params);
1156 mTextEdit.setLayoutParams(params);
1159 mTextEdit.setVisibility(View.VISIBLE);
1160 mTextEdit.requestFocus();
1162 InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1163 imm.showSoftInput(mTextEdit, 0);
1165 mScreenKeyboardShown = true;
1170 * This method is called by SDL using JNI.
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));
1177 public static boolean isTextInputEvent(KeyEvent event) {
1179 // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT
1180 if (event.isCtrlPressed()) {
1184 return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE;
1188 * This method is called by SDL using JNI.
1190 public static Surface getNativeSurface() {
1191 if (SDLActivity.mSurface == null) {
1194 return SDLActivity.mSurface.getNativeSurface();
1200 * This method is called by SDL using JNI.
1202 public static void initTouch() {
1203 int[] ids = InputDevice.getDeviceIds();
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());
1215 /** Result of current messagebox. Also used for blocking the calling thread. */
1216 protected final int[] messageboxSelection = new int[1];
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.
1228 public int messageboxShowMessageBox(
1231 final String message,
1232 final int[] buttonFlags,
1233 final int[] buttonIds,
1234 final String[] buttonTexts,
1235 final int[] colors) {
1237 messageboxSelection[0] = -1;
1241 if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) {
1242 return -1; // implementation broken
1245 // collect arguments for Dialog
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);
1256 // trigger Dialog creation on UI thread
1258 runOnUiThread(new Runnable() {
1261 messageboxCreateAndShow(args);
1265 // block the calling thread
1267 synchronized (messageboxSelection) {
1269 messageboxSelection.wait();
1270 } catch (InterruptedException ex) {
1271 ex.printStackTrace();
1276 // return selected value
1278 return messageboxSelection[0];
1281 protected void messageboxCreateAndShow(Bundle args) {
1283 // TODO set values from "flags" to messagebox dialog
1287 int[] colors = args.getIntArray("colors");
1288 int backgroundColor;
1290 int buttonBorderColor;
1291 int buttonBackgroundColor;
1292 int buttonSelectedColor;
1293 if (colors != null) {
1295 backgroundColor = colors[++i];
1296 textColor = colors[++i];
1297 buttonBorderColor = colors[++i];
1298 buttonBackgroundColor = colors[++i];
1299 buttonSelectedColor = colors[++i];
1301 backgroundColor = Color.TRANSPARENT;
1302 textColor = Color.TRANSPARENT;
1303 buttonBorderColor = Color.TRANSPARENT;
1304 buttonBackgroundColor = Color.TRANSPARENT;
1305 buttonSelectedColor = Color.TRANSPARENT;
1308 // create dialog with title and a listener to wake up calling thread
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() {
1315 public void onDismiss(DialogInterface unused) {
1316 synchronized (messageboxSelection) {
1317 messageboxSelection.notify();
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);
1333 int[] buttonFlags = args.getIntArray("buttonFlags");
1334 int[] buttonIds = args.getIntArray("buttonIds");
1335 String[] buttonTexts = args.getStringArray("buttonTexts");
1337 final SparseArray<Button> mapping = new SparseArray<Button>();
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() {
1347 public void onClick(View v) {
1348 messageboxSelection[0] = id;
1352 if (buttonFlags[i] != 0) {
1353 // see SDL_messagebox.h
1354 if ((buttonFlags[i] & 0x00000001) != 0) {
1355 mapping.put(KeyEvent.KEYCODE_ENTER, button);
1357 if ((buttonFlags[i] & 0x00000002) != 0) {
1358 mapping.put(KeyEvent.KEYCODE_ESCAPE, button); /* API 11 */
1361 button.setText(buttonTexts[i]);
1362 if (textColor != Color.TRANSPARENT) {
1363 button.setTextColor(textColor);
1365 if (buttonBorderColor != Color.TRANSPARENT) {
1366 // TODO set color for border of messagebox button
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);
1374 // setting the color this way keeps the style (gradient, padding, etc.)
1375 drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY);
1378 if (buttonSelectedColor != Color.TRANSPARENT) {
1379 // TODO set color for selected messagebox button
1381 buttons.addView(button);
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);
1394 // add content to dialog and return
1396 dialog.setView(content);
1397 dialog.setOnKeyListener(new Dialog.OnKeyListener() {
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();
1405 return true; // also for ignored actions
1414 private final Runnable rehideSystemUi = new Runnable() {
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;
1425 SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags);
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)) {
1433 Handler handler = getWindow().getDecorView().getHandler();
1434 if (handler != null) {
1435 handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop.
1436 handler.postDelayed(rehideSystemUi, 2000);
1443 * This method is called by SDL using JNI.
1445 public static boolean clipboardHasText() {
1446 return mClipboardHandler.clipboardHasText();
1450 * This method is called by SDL using JNI.
1452 public static String clipboardGetText() {
1453 return mClipboardHandler.clipboardGetText();
1457 * This method is called by SDL using JNI.
1459 public static void clipboardSetText(String string) {
1460 mClipboardHandler.clipboardSetText(string);
1464 * This method is called by SDL using JNI.
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);
1470 if (Build.VERSION.SDK_INT >= 24) {
1472 mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY));
1473 } catch (Exception e) {
1479 return mLastCursorID;
1483 * This method is called by SDL using JNI.
1485 public static void destroyCustomCursor(int cursorID) {
1486 if (Build.VERSION.SDK_INT >= 24) {
1488 mCursors.remove(cursorID);
1489 } catch (Exception e) {
1496 * This method is called by SDL using JNI.
1498 public static boolean setCustomCursor(int cursorID) {
1500 if (Build.VERSION.SDK_INT >= 24) {
1502 mSurface.setPointerIcon(mCursors.get(cursorID));
1503 } catch (Exception e) {
1513 * This method is called by SDL using JNI.
1515 public static boolean setSystemCursor(int cursorID) {
1516 int cursor_type = 0; //PointerIcon.TYPE_NULL;
1518 case SDL_SYSTEM_CURSOR_ARROW:
1519 cursor_type = 1000; //PointerIcon.TYPE_ARROW;
1521 case SDL_SYSTEM_CURSOR_IBEAM:
1522 cursor_type = 1008; //PointerIcon.TYPE_TEXT;
1524 case SDL_SYSTEM_CURSOR_WAIT:
1525 cursor_type = 1004; //PointerIcon.TYPE_WAIT;
1527 case SDL_SYSTEM_CURSOR_CROSSHAIR:
1528 cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR;
1530 case SDL_SYSTEM_CURSOR_WAITARROW:
1531 cursor_type = 1004; //PointerIcon.TYPE_WAIT;
1533 case SDL_SYSTEM_CURSOR_SIZENWSE:
1534 cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
1536 case SDL_SYSTEM_CURSOR_SIZENESW:
1537 cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
1539 case SDL_SYSTEM_CURSOR_SIZEWE:
1540 cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
1542 case SDL_SYSTEM_CURSOR_SIZENS:
1543 cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
1545 case SDL_SYSTEM_CURSOR_SIZEALL:
1546 cursor_type = 1020; //PointerIcon.TYPE_GRAB;
1548 case SDL_SYSTEM_CURSOR_NO:
1549 cursor_type = 1012; //PointerIcon.TYPE_NO_DROP;
1551 case SDL_SYSTEM_CURSOR_HAND:
1552 cursor_type = 1002; //PointerIcon.TYPE_HAND;
1555 if (Build.VERSION.SDK_INT >= 24) {
1557 mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type));
1558 } catch (Exception e) {
1566 * This method is called by SDL using JNI.
1568 public static void requestPermission(String permission, int requestCode) {
1569 if (Build.VERSION.SDK_INT < 23) {
1570 nativePermissionResult(requestCode, true);
1574 Activity activity = (Activity)getContext();
1575 if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
1576 activity.requestPermissions(new String[]{permission}, requestCode);
1578 nativePermissionResult(requestCode, true);
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);
1589 * This method is called by SDL using JNI.
1591 public static int openURL(String url)
1594 Intent i = new Intent(Intent.ACTION_VIEW);
1595 i.setData(Uri.parse(url));
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;
1601 flags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
1605 mSingleton.startActivity(i);
1606 } catch (Exception ex) {
1613 * This method is called by SDL using JNI.
1615 public static int showToast(String message, int duration, int gravity, int xOffset, int yOffset)
1617 if(null == mSingleton) {
1623 class OneShotTask implements Runnable {
1630 OneShotTask(String message, int duration, int gravity, int xOffset, int yOffset) {
1632 mDuration = duration;
1641 Toast toast = Toast.makeText(mSingleton, mMessage, mDuration);
1642 if (mGravity >= 0) {
1643 toast.setGravity(mGravity, mXOffset, mYOffset);
1646 } catch(Exception ex) {
1647 Log.e(TAG, ex.getMessage());
1651 mSingleton.runOnUiThread(new OneShotTask(message, duration, gravity, xOffset, yOffset));
1652 } catch(Exception ex) {
1660 Simple runnable to start the SDL application
1662 class SDLMain implements Runnable {
1666 String library = SDLActivity.mSingleton.getMainSharedObject();
1667 String function = SDLActivity.mSingleton.getMainFunction();
1668 String[] arguments = SDLActivity.mSingleton.getArguments();
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());
1676 Log.v("SDL", "Running main function " + function + " from library " + library);
1678 SDLActivity.nativeRunMain(library, function, arguments);
1680 Log.v("SDL", "Finished main function");
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
1693 SDLSurface. This is what we draw on, so we need to know when it's created
1694 in order to do anything useful.
1696 Because of this, that's where we set up the SDL thread
1698 class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
1699 View.OnKeyListener, View.OnTouchListener, SensorEventListener {
1702 protected SensorManager mSensorManager;
1703 protected Display mDisplay;
1705 // Keep track of the surface size to normalize touch events
1706 protected float mWidth, mHeight;
1708 // Is SurfaceView ready for rendering
1709 public boolean mIsSurfaceReady;
1712 public SDLSurface(Context context) {
1714 getHolder().addCallback(this);
1717 setFocusableInTouchMode(true);
1719 setOnKeyListener(this);
1720 setOnTouchListener(this);
1722 mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
1723 mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
1725 setOnGenericMotionListener(SDLActivity.getMotionListener());
1727 // Some arbitrary defaults to avoid a potential division by zero
1731 mIsSurfaceReady = false;
1734 public void handlePause() {
1735 enableSensor(Sensor.TYPE_ACCELEROMETER, false);
1738 public void handleResume() {
1740 setFocusableInTouchMode(true);
1742 setOnKeyListener(this);
1743 setOnTouchListener(this);
1744 enableSensor(Sensor.TYPE_ACCELEROMETER, true);
1747 public Surface getNativeSurface() {
1748 return getHolder().getSurface();
1751 // Called when we have a valid drawing surface
1753 public void surfaceCreated(SurfaceHolder holder) {
1754 Log.v("SDL", "surfaceCreated()");
1755 SDLActivity.onNativeSurfaceCreated();
1758 // Called when we lose the surface
1760 public void surfaceDestroyed(SurfaceHolder holder) {
1761 Log.v("SDL", "surfaceDestroyed()");
1763 // Transition to pause, if needed
1764 SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
1765 SDLActivity.handleNativeState();
1767 mIsSurfaceReady = false;
1768 SDLActivity.onNativeSurfaceDestroyed();
1771 // Called when the surface is resized
1773 public void surfaceChanged(SurfaceHolder holder,
1774 int format, int width, int height) {
1775 Log.v("SDL", "surfaceChanged()");
1777 if (SDLActivity.mSingleton == null) {
1783 int nDeviceWidth = width;
1784 int nDeviceHeight = height;
1787 if (Build.VERSION.SDK_INT >= 17) {
1788 DisplayMetrics realMetrics = new DisplayMetrics();
1789 mDisplay.getRealMetrics( realMetrics );
1790 nDeviceWidth = realMetrics.widthPixels;
1791 nDeviceHeight = realMetrics.heightPixels;
1793 } catch(Exception ignored) {
1796 synchronized(SDLActivity.getContext()) {
1797 // In case we're waiting on a size change after going fullscreen, send a notification.
1798 SDLActivity.getContext().notifyAll();
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();
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();
1811 if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
1812 if (mWidth > mHeight) {
1815 } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
1816 if (mWidth < mHeight) {
1821 // Special Patch for Square Resolution: Black Berry Passport
1823 double min = Math.min(mWidth, mHeight);
1824 double max = Math.max(mWidth, mHeight);
1826 if (max / min < 1.20) {
1827 Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
1832 // Don't skip in MultiWindow.
1834 if (Build.VERSION.SDK_INT >= 24) {
1835 if (SDLActivity.mSingleton.isInMultiWindowMode()) {
1836 Log.v("SDL", "Don't skip in Multi-Window");
1843 Log.v("SDL", "Skip .. Surface is not ready.");
1844 mIsSurfaceReady = false;
1848 /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
1849 SDLActivity.onNativeSurfaceChanged();
1851 /* Surface is ready */
1852 mIsSurfaceReady = true;
1854 SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
1855 SDLActivity.handleNativeState();
1860 public boolean onKey(View v, int keyCode, KeyEvent event) {
1862 int deviceId = event.getDeviceId();
1863 int source = event.getSource();
1865 if (source == InputDevice.SOURCE_UNKNOWN) {
1866 InputDevice device = InputDevice.getDevice(deviceId);
1867 if (device != null) {
1868 source = device.getSources();
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);
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
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) {
1891 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1892 if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) {
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);
1903 SDLActivity.onNativeKeyDown(keyCode);
1905 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1906 SDLActivity.onNativeKeyUp(keyCode);
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()
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;
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
1945 if (touchDevId < 0) {
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;
1955 Object object = event.getClass().getMethod("getButtonState").invoke(event);
1956 if (object != null) {
1957 mouseButton = (Integer) object;
1959 } catch(Exception ignored) {
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);
1968 SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
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);
1978 // may be larger than 1.0f on some devices
1979 // see the documentation of getPressure(i)
1982 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
1986 case MotionEvent.ACTION_UP:
1987 case MotionEvent.ACTION_DOWN:
1988 // Primary pointer up/down, the index is always zero
1991 case MotionEvent.ACTION_POINTER_UP:
1992 case MotionEvent.ACTION_POINTER_DOWN:
1993 // Non primary pointer up/down
1995 i = event.getActionIndex();
1998 pointerFingerId = event.getPointerId(i);
1999 x = event.getX(i) / mWidth;
2000 y = event.getY(i) / mHeight;
2001 p = event.getPressure(i);
2003 // may be larger than 1.0f on some devices
2004 // see the documentation of getPressure(i)
2007 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
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);
2017 // may be larger than 1.0f on some devices
2018 // see the documentation of getPressure(i)
2021 SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
2034 public void enableSensor(int sensortype, boolean enabled) {
2035 // TODO: This uses getDefaultSensor - what if we have >1 accels?
2037 mSensorManager.registerListener(this,
2038 mSensorManager.getDefaultSensor(sensortype),
2039 SensorManager.SENSOR_DELAY_GAME, null);
2041 mSensorManager.unregisterListener(this,
2042 mSensorManager.getDefaultSensor(sensortype));
2047 public void onAccuracyChanged(Sensor sensor, int accuracy) {
2052 public void onSensorChanged(SensorEvent event) {
2053 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
2055 // Since we may have an orientation set, we won't receive onConfigurationChanged events.
2056 // We thus should check here.
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;
2066 case Surface.ROTATION_270:
2067 x = event.values[1];
2068 y = -event.values[0];
2069 newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
2071 case Surface.ROTATION_180:
2072 x = -event.values[0];
2073 y = -event.values[1];
2074 newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
2076 case Surface.ROTATION_0:
2078 x = event.values[0];
2079 y = event.values[1];
2080 newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
2084 if (newOrientation != SDLActivity.mCurrentOrientation) {
2085 SDLActivity.mCurrentOrientation = newOrientation;
2086 SDLActivity.onNativeOrientationChanged(newOrientation);
2089 SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
2090 y / SensorManager.GRAVITY_EARTH,
2091 event.values[2] / SensorManager.GRAVITY_EARTH);
2097 // Captured pointer events for API 26.
2098 public boolean onCapturedPointerEvent(MotionEvent event)
2100 int action = event.getActionMasked();
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);
2110 case MotionEvent.ACTION_HOVER_MOVE:
2111 case MotionEvent.ACTION_MOVE:
2114 SDLActivity.onNativeMouse(0, action, x, y, true);
2117 case MotionEvent.ACTION_BUTTON_PRESS:
2118 case MotionEvent.ACTION_BUTTON_RELEASE:
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;
2129 int button = event.getButtonState();
2131 SDLActivity.onNativeMouse(button, action, x, y, true);
2140 /* This is a fake invisible editor view that receives the input and defines the
2143 class DummyEdit extends View implements View.OnKeyListener {
2146 public DummyEdit(Context context) {
2148 setFocusableInTouchMode(true);
2150 setOnKeyListener(this);
2154 public boolean onCheckIsTextEditor() {
2159 public boolean onKey(View v, int keyCode, KeyEvent event) {
2161 * This handles the hardware keyboard input
2163 if (event.getAction() == KeyEvent.ACTION_DOWN) {
2164 if (SDLActivity.isTextInputEvent(event)) {
2165 ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
2168 SDLActivity.onNativeKeyDown(keyCode);
2170 } else if (event.getAction() == KeyEvent.ACTION_UP) {
2171 SDLActivity.onNativeKeyUp(keyCode);
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();
2191 return super.onKeyPreIme(keyCode, event);
2195 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
2196 ic = new SDLInputConnection(this, true);
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 */;
2206 class SDLInputConnection extends BaseInputConnection {
2208 public SDLInputConnection(View targetView, boolean fullEditor) {
2209 super(targetView, fullEditor);
2214 public boolean sendKeyEvent(KeyEvent event) {
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.
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.
2227 if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
2228 if (SDLActivity.onNativeSoftReturnKey()) {
2234 return super.sendKeyEvent(event);
2238 public boolean commitText(CharSequence text, int newCursorPosition) {
2240 for (int i = 0; i < text.length(); i++) {
2241 char c = text.charAt(i);
2243 if (SDLActivity.onNativeSoftReturnKey()) {
2247 nativeGenerateScancodeForUnichar(c);
2250 SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition);
2252 return super.commitText(text, newCursorPosition);
2256 public boolean setComposingText(CharSequence text, int newCursorPosition) {
2258 nativeSetComposingText(text.toString(), newCursorPosition);
2260 return super.setComposingText(text, newCursorPosition);
2263 public static native void nativeCommitText(String text, int newCursorPosition);
2265 public native void nativeGenerateScancodeForUnichar(char c);
2267 public native void nativeSetComposingText(String text, int newCursorPosition);
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) {
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;
2284 return super.deleteSurroundingText(beforeLength, afterLength);
2288 class SDLClipboardHandler implements
2289 ClipboardManager.OnPrimaryClipChangedListener {
2291 protected ClipboardManager mClipMgr;
2293 SDLClipboardHandler() {
2294 mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
2295 mClipMgr.addPrimaryClipChangedListener(this);
2298 public boolean clipboardHasText() {
2299 return mClipMgr.hasPrimaryClip();
2302 public String clipboardGetText() {
2303 ClipData clip = mClipMgr.getPrimaryClip();
2305 ClipData.Item item = clip.getItemAt(0);
2307 CharSequence text = item.getText();
2309 return text.toString();
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);
2324 public void onPrimaryClipChanged() {
2325 SDLActivity.onNativeClipboardChanged();