From eca89e73b29ca98e1e0b9dda8c3297080e227965 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Fri, 15 Jan 2016 18:03:00 +0100 Subject: [PATCH] SDL2: implement support for loadingscreen As explained on #465, the previous approach (pure GL, or using window background) doesn't fully work. This approach have a tiny delay until PythonActivity.onCreate is called. It first display an ImageView fitted on the screen. Once SDL is loaded (and that SDL put its own layout), we put again our ImageView. The tricky part is when to remove. On the previous python-for-android+kivy, kivy was explicitely saying that he's ready to receive event. And that's where we knew the loading screen could be removed. Here, there is no absolute way to do it, as SDL itself on the java part doesn't provide any callback... except pollInputDevices() This method is called each X seconds, to check if any joystick has been plugged or not. The PR add a keepActive() method that is overrided in PythonActivity to remove the screen. It wait for the second call of it before removing the loading screen: 1. the first call is done right after the creation of the SDL window in C code. (In kivy, it's just the start, after that we load the app, widgets etc... it can take a long time) 2. the second call is done (at least on kivy) after the first frame rendered. The loading screen might be displayed more than needed (pure SDL2 game // very fast loading). In any case, we can always force to remove the loading screen with: PythonActivity.mActivity.removeLoadingScreen() If anybody have another good approach, feel free :) Closes #465 --- .../src/org/kivy/android/PythonActivity.java | 99 ++++++++++++-- .../build/src/org/libsdl/app/SDLActivity.java | 129 +++++++++--------- 2 files changed, 156 insertions(+), 72 deletions(-) diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java index e519971d3a..3fa0e6ddf5 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/kivy/android/PythonActivity.java @@ -25,6 +25,10 @@ import android.content.pm.PackageManager; import android.content.pm.ApplicationInfo; import android.content.Intent; +import android.widget.ImageView; +import java.io.InputStream; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import org.libsdl.app.SDLActivity; @@ -38,7 +42,7 @@ public class PythonActivity extends SDLActivity { private static final String TAG = "PythonActivity"; public static PythonActivity mActivity = null; - + private ResourceManager resourceManager = null; private Bundle mMetaData = null; private PowerManager.WakeLock mWakeLock = null; @@ -47,6 +51,7 @@ public class PythonActivity extends SDLActivity { protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "My oncreate running"); resourceManager = new ResourceManager(this); + this.showLoadingScreen(); Log.v(TAG, "Ready to unpack"); unpackData("private", getFilesDir()); @@ -54,9 +59,10 @@ protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "About to do super onCreate"); super.onCreate(savedInstanceState); Log.v(TAG, "Did super onCreate"); - + + this.showLoadingScreen(); this.mActivity = this; - + String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); Log.v(TAG, "Setting env vars for start.c and Python to use"); SDLActivity.nativeSetEnv("ANDROID_PRIVATE", mFilesDirectory); @@ -65,7 +71,7 @@ protected void onCreate(Bundle savedInstanceState) { SDLActivity.nativeSetEnv("PYTHONHOME", mFilesDirectory); SDLActivity.nativeSetEnv("PYTHONPATH", mFilesDirectory + ":" + mFilesDirectory + "/lib"); - + // nativeSetEnv("ANDROID_ARGUMENT", getFilesDir()); try { @@ -87,11 +93,11 @@ protected void onCreate(Bundle savedInstanceState) { } catch (PackageManager.NameNotFoundException e) { } } - + public void loadLibraries() { PythonUtil.loadLibraries(getFilesDir()); } - + public void recursiveDelete(File f) { if (f.isDirectory()) { for (File r : f.listFiles()) { @@ -123,15 +129,15 @@ public void run() { } } } - + public void unpackData(final String resource, File target) { - + Log.v(TAG, "UNPACKING!!! " + resource + " " + target.getName()); - + // The version of data in memory and on disk. String data_version = resourceManager.getString(resource + "_version"); String disk_version = null; - + Log.v(TAG, "Data version is " + data_version); // If no version, no unpacking is necessary. @@ -180,7 +186,7 @@ public void unpackData(final String resource, File target) { } } } - + public static ViewGroup getLayout() { return mLayout; } @@ -277,4 +283,75 @@ public static void stop_service() { Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); PythonActivity.mActivity.stopService(serviceIntent); } + + /** Loading screen implementation + * keepActive() is a method plugged in pollInputDevices in SDLActivity. + * Once it's called twice, the loading screen will be removed. + * The first call happen as soon as the window is created, but no image has been + * displayed first. My tests showed that we can wait one more. This might delay + * the real available of few hundred milliseconds. + * The real deal is to know if a rendering has already happen. The previous + * python-for-android and kivy was having something for that, but this new version + * is not compatible, and would require a new kivy version. + * In case of, the method PythonActivty.mActivity.removeLoadingScreen() can be called. + */ + public static ImageView mImageView = null; + int mLoadingCount = 2; + + @Override + public void keepActive() { + Log.v("python", "keepActive from PythonActivity"); + if (this.mLoadingCount > 0) { + this.mLoadingCount -= 1; + if (this.mLoadingCount == 0) { + this.removeLoadingScreen(); + } + } + } + + public void removeLoadingScreen() { + runOnUiThread(new Runnable() { + public void run() { + if (PythonActivity.mImageView != null) { + ((ViewGroup)PythonActivity.mImageView.getParent()).removeView( + PythonActivity.mImageView); + PythonActivity.mImageView = null; + } + } + }); + } + + protected void showLoadingScreen() { + // load the bitmap + // 1. if the image is valid and we don't have layout yet, assign this bitmap + // as main view. + // 2. if we have a layout, just set it in the layout. + if (mImageView == null) { + int presplashId = this.resourceManager.getIdentifier("presplash", "drawable"); + InputStream is = this.getResources().openRawResource(presplashId); + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeStream(is); + } finally { + try { + is.close(); + } catch (IOException e) {}; + } + + mImageView = new ImageView(this); + mImageView.setImageBitmap(bitmap); + mImageView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + } + + if (mLayout == null) { + setContentView(mImageView); + } else { + mLayout.addView(mImageView); + } + } + + } diff --git a/pythonforandroid/bootstraps/sdl2/build/src/org/libsdl/app/SDLActivity.java b/pythonforandroid/bootstraps/sdl2/build/src/org/libsdl/app/SDLActivity.java index 76134e8274..6262715050 100644 --- a/pythonforandroid/bootstraps/sdl2/build/src/org/libsdl/app/SDLActivity.java +++ b/pythonforandroid/bootstraps/sdl2/build/src/org/libsdl/app/SDLActivity.java @@ -54,7 +54,7 @@ public class SDLActivity extends Activity { // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; - + // Audio protected static AudioTrack mAudioTrack; @@ -83,7 +83,7 @@ public void loadLibraries() { System.loadLibrary(lib); } } - + /** * This method is called by SDL before starting the native application thread. * It can be overridden to provide the arguments after the application name. @@ -93,7 +93,7 @@ public void loadLibraries() { protected String[] getArguments() { return new String[0]; } - + public static void initialize() { // The static nature of the singleton and Android quirkyness force us to initialize everything here // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values @@ -114,11 +114,11 @@ public static void initialize() { // Setup @Override protected void onCreate(Bundle savedInstanceState) { - Log.v("SDL", "Device: " + android.os.Build.DEVICE); + Log.v("SDL", "Device: " + android.os.Build.DEVICE); Log.v("SDL", "Model: " + android.os.Build.MODEL); Log.v("SDL", "onCreate():" + mSingleton); super.onCreate(savedInstanceState); - + SDLActivity.initialize(); // So we can call stuff from static callbacks mSingleton = this; @@ -161,7 +161,7 @@ public void onClick(DialogInterface dialog,int id) { // Set up the surface mSurface = new SDLSurface(getApplication()); - + if(Build.VERSION.SDK_INT >= 12) { mJoystickHandler = new SDLJoystickHandler_API12(); } @@ -254,7 +254,7 @@ protected void onDestroy() { //Log.v("SDL", "Finished waiting for SDL thread"); } - + super.onDestroy(); // Reset everything in case the user re opens the app SDLActivity.initialize(); @@ -303,7 +303,7 @@ public static void handleResume() { mSurface.handleResume(); } } - + /* The native thread has finished */ public static void handleNativeExit() { SDLActivity.mSDLThread = null; @@ -410,14 +410,14 @@ public static native void onNativeHat(int device_id, int hat_id, public static native void onNativeKeyboardFocusLost(); public static native void onNativeMouse(int button, int action, float x, float y); public static native void onNativeTouch(int touchDevId, int pointerFingerId, - int action, float x, + int action, float x, float y, float p); public static native void onNativeAccel(float x, float y, float z); public static native void onNativeSurfaceChanged(); public static native void onNativeSurfaceDestroyed(); public static native void nativeFlipBuffers(); - public static native int nativeAddJoystick(int device_id, String name, - int is_accelerometer, int nbuttons, + public static native int nativeAddJoystick(int device_id, String name, + int is_accelerometer, int nbuttons, int naxes, int nhats, int nballs); public static native int nativeRemoveJoystick(int device_id); public static native String nativeGetHint(String name); @@ -542,33 +542,33 @@ public static int audioInit(int sampleRate, boolean is16Bit, boolean isStereo, i int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO; int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT; int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1); - + Log.v("SDL", "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); - + // Let the user pick a larger buffer if they really want -- but ye // gods they probably shouldn't, the minimums are horrifyingly high // latency already desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); - + if (mAudioTrack == null) { mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); - + // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() - + if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { Log.e("SDL", "Failed during initialization of Audio Track"); mAudioTrack = null; return -1; } - + mAudioTrack.play(); } - + Log.v("SDL", "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"); - + return 0; } @@ -654,9 +654,16 @@ public static boolean handleJoystickMotionEvent(MotionEvent event) { public static void pollInputDevices() { if (SDLActivity.mSDLThread != null) { mJoystickHandler.pollInputDevices(); + SDLActivity.mSingleton.keepActive(); } } + /** + * Trick needed for loading screen + */ + public void keepActive() { + } + // APK extension files support /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ @@ -928,11 +935,11 @@ public void run() { /** SDLSurface. This is what we draw on, so we need to know when it's created - in order to do anything useful. + in order to do anything useful. Because of this, that's where we set up the SDL thread */ -class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, +class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, View.OnKeyListener, View.OnTouchListener, SensorEventListener { // Sensors @@ -942,20 +949,20 @@ class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, // Keep track of the surface size to normalize touch events protected static float mWidth, mHeight; - // Startup + // Startup public SDLSurface(Context context) { super(context); - getHolder().addCallback(this); - + getHolder().addCallback(this); + setFocusable(true); setFocusableInTouchMode(true); requestFocus(); - setOnKeyListener(this); - setOnTouchListener(this); + setOnKeyListener(this); + setOnTouchListener(this); mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - + if(Build.VERSION.SDK_INT >= 12) { setOnGenericMotionListener(new SDLGenericMotionListener_API12()); } @@ -964,7 +971,7 @@ public SDLSurface(Context context) { mWidth = 1.0f; mHeight = 1.0f; } - + public void handleResume() { setFocusable(true); setFocusableInTouchMode(true); @@ -973,7 +980,7 @@ public void handleResume() { setOnTouchListener(this); enableSensor(Sensor.TYPE_ACCELEROMETER, true); } - + public Surface getNativeSurface() { return getHolder().getSurface(); } @@ -1063,7 +1070,7 @@ public void surfaceChanged(SurfaceHolder holder, final Thread sdlThread = new Thread(new SDLMain(), "SDLThread"); enableSensor(Sensor.TYPE_ACCELEROMETER, true); sdlThread.start(); - + // Set up a listener thread to catch when the native thread ends SDLActivity.mSDLThread = new Thread(new Runnable(){ @Override @@ -1072,7 +1079,7 @@ public void run(){ sdlThread.join(); } catch(Exception e){} - finally{ + finally{ // Native thread has finished if (! SDLActivity.mExitCalledFromJava) { SDLActivity.handleNativeExit(); @@ -1108,7 +1115,7 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { } } } - + if( (event.getSource() & InputDevice.SOURCE_KEYBOARD) != 0) { if (event.getAction() == KeyEvent.ACTION_DOWN) { //Log.v("SDL", "key down: " + keyCode); @@ -1121,7 +1128,7 @@ else if (event.getAction() == KeyEvent.ACTION_UP) { return true; } } - + return false; } @@ -1160,7 +1167,7 @@ public boolean onTouch(View v, MotionEvent event) { SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); } break; - + case MotionEvent.ACTION_UP: case MotionEvent.ACTION_DOWN: // Primary pointer up/down, the index is always zero @@ -1171,14 +1178,14 @@ public boolean onTouch(View v, MotionEvent event) { if (i == -1) { i = event.getActionIndex(); } - + pointerFingerId = event.getPointerId(i); x = event.getX(i) / mWidth; y = event.getY(i) / mHeight; p = event.getPressure(i); SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); break; - + case MotionEvent.ACTION_CANCEL: for (i = 0; i < pointerCount; i++) { pointerFingerId = event.getPointerId(i); @@ -1195,21 +1202,21 @@ public boolean onTouch(View v, MotionEvent event) { } return true; - } + } // Sensor events public void enableSensor(int sensortype, boolean enabled) { // TODO: This uses getDefaultSensor - what if we have >1 accels? if (enabled) { - mSensorManager.registerListener(this, - mSensorManager.getDefaultSensor(sensortype), + mSensorManager.registerListener(this, + mSensorManager.getDefaultSensor(sensortype), SensorManager.SENSOR_DELAY_GAME, null); } else { - mSensorManager.unregisterListener(this, + mSensorManager.unregisterListener(this, mSensorManager.getDefaultSensor(sensortype)); } } - + @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // TODO @@ -1241,7 +1248,7 @@ public void onSensorChanged(SensorEvent event) { y / SensorManager.GRAVITY_EARTH, event.values[2] / SensorManager.GRAVITY_EARTH - 1); } - } + } } /* This is a fake invisible editor view that receives the input and defines the @@ -1283,7 +1290,7 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { return false; } - + // @Override public boolean onKeyPreIme (int keyCode, KeyEvent event) { @@ -1362,7 +1369,7 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) { public native void nativeSetComposingText(String text, int newCursorPosition); @Override - public boolean deleteSurroundingText(int beforeLength, int afterLength) { + public boolean deleteSurroundingText(int beforeLength, int afterLength) { // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection if (beforeLength == 1 && afterLength == 0) { // backspace @@ -1376,7 +1383,7 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) { /* A null joystick handler for API level < 12 devices (the accelerometer is handled separately) */ class SDLJoystickHandler { - + /** * Handles given MotionEvent. * @param event the event to be handled. @@ -1408,11 +1415,11 @@ public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { return arg0.getAxis() - arg1.getAxis(); } } - + private ArrayList mJoysticks; - + public SDLJoystickHandler_API12() { - + mJoysticks = new ArrayList(); } @@ -1423,7 +1430,7 @@ public void pollInputDevices() { // For example, in the case of the XBox 360 wireless dongle, // so the first controller seen by SDL matches what the receiver // considers to be the first controller - + for(int i=deviceIds.length-1; i>-1; i--) { SDLJoystick joystick = getJoystick(deviceIds[i]); if (joystick == null) { @@ -1434,7 +1441,7 @@ public void pollInputDevices() { joystick.name = joystickDevice.getName(); joystick.axes = new ArrayList(); joystick.hats = new ArrayList(); - + List ranges = joystickDevice.getMotionRanges(); Collections.sort(ranges, new RangeComparator()); for (InputDevice.MotionRange range : ranges ) { @@ -1448,14 +1455,14 @@ public void pollInputDevices() { } } } - + mJoysticks.add(joystick); - SDLActivity.nativeAddJoystick(joystick.device_id, joystick.name, 0, -1, + SDLActivity.nativeAddJoystick(joystick.device_id, joystick.name, 0, -1, joystick.axes.size(), joystick.hats.size()/2, 0); } } } - + /* Check removed devices */ ArrayList removedDevices = new ArrayList(); for(int i=0; i < mJoysticks.size(); i++) { @@ -1468,7 +1475,7 @@ public void pollInputDevices() { removedDevices.add(Integer.valueOf(device_id)); } } - + for(int i=0; i < removedDevices.size(); i++) { int device_id = removedDevices.get(i).intValue(); SDLActivity.nativeRemoveJoystick(device_id); @@ -1478,9 +1485,9 @@ public void pollInputDevices() { break; } } - } + } } - + protected SDLJoystick getJoystick(int device_id) { for(int i=0; i < mJoysticks.size(); i++) { if (mJoysticks.get(i).device_id == device_id) { @@ -1488,9 +1495,9 @@ protected SDLJoystick getJoystick(int device_id) { } } return null; - } - - @Override + } + + @Override public boolean handleMotionEvent(MotionEvent event) { if ( (event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) { int actionPointerIndex = event.getActionIndex(); @@ -1504,7 +1511,7 @@ public boolean handleMotionEvent(MotionEvent event) { /* Normalize the value to -1...1 */ float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f; SDLActivity.onNativeJoy(joystick.device_id, i, value ); - } + } for (int i = 0; i < joystick.hats.size(); i+=2) { int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) ); int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) ); @@ -1517,7 +1524,7 @@ public boolean handleMotionEvent(MotionEvent event) { } } return true; - } + } } class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {