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