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