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