diff --git a/project/java/CloudSave.java b/project/java/CloudSave.java index 4e4d72c37..80f6a2441 100644 --- a/project/java/CloudSave.java +++ b/project/java/CloudSave.java @@ -39,4 +39,14 @@ class CloudSave public void onActivityResult(int request, int response, Intent data) { } + + public boolean save(String filename, String saveId, String dialogTitle, String description, String imageFile, long playedTimeMs) + { + return false; + } + + public boolean load(String filename, String saveId, String dialogTitle) + { + return false; + } } diff --git a/project/java/MainActivity.java b/project/java/MainActivity.java index cef4d85b3..7f5b5cd9f 100644 --- a/project/java/MainActivity.java +++ b/project/java/MainActivity.java @@ -80,6 +80,8 @@ import android.view.Display; import android.text.InputType; import android.util.Log; import android.view.Surface; +import android.app.ProgressDialog; + public class MainActivity extends Activity @@ -106,6 +108,8 @@ public class MainActivity extends Activity _layout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); _layout2 = new LinearLayout(this); _layout2.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + loadingDialog = new ProgressDialog(this); + loadingDialog.setMessage(getString(R.string.accessing_network)); final Semaphore loadedLibraries = new Semaphore(0); @@ -220,7 +224,7 @@ public class MainActivity extends Activity Intent intent = new Intent(this, DummyService.class); startService(intent); } - _cloudSave = new CloudSave(this); + cloudSave = new CloudSave(this); } public void setUpStatusLabel() @@ -429,19 +433,19 @@ public class MainActivity extends Activity @Override protected void onStart() { super.onStart(); - _cloudSave.onStart(); + cloudSave.onStart(); } @Override protected void onStop() { super.onStart(); - _cloudSave.onStop(); + cloudSave.onStop(); } @Override public void onActivityResult(int request, int response, Intent data) { super.onActivityResult(request, response, data); - _cloudSave.onActivityResult(request, response, data); + cloudSave.onActivityResult(request, response, data); } public void showScreenKeyboardWithoutTextInputField() @@ -1235,7 +1239,8 @@ public class MainActivity extends Activity private LinearLayout _layout = null; private LinearLayout _layout2 = null; private Advertisement _ad = null; - public CloudSave _cloudSave = null; + public CloudSave cloudSave = null; + public ProgressDialog loadingDialog = null; private FrameLayout _videoLayout = null; private EditText _screenKeyboard = null; diff --git a/project/java/Video.java b/project/java/Video.java index e19b970fd..b061267cf 100644 --- a/project/java/Video.java +++ b/project/java/Video.java @@ -834,6 +834,52 @@ class DemoRenderer extends GLSurfaceView_SDL.Renderer context.requestNewAdvertisement(); } + public boolean cloudSave(String filename, String saveId, String dialogTitle, String description, String imageFile, long playedTimeMs) + { + context.runOnUiThread(new Runnable() + { + public void run() + { + context.loadingDialog.show(); + } + }); + + boolean ret = context.cloudSave.save(filename, saveId, dialogTitle, description, imageFile, playedTimeMs); + + context.runOnUiThread(new Runnable() + { + public void run() + { + context.loadingDialog.dismiss(); + } + }); + + return ret; + } + + public boolean cloudLoad(String filename, String saveId, String dialogTitle) + { + context.runOnUiThread(new Runnable() + { + public void run() + { + context.loadingDialog.show(); + } + }); + + boolean ret = context.cloudSave.load(filename, saveId, dialogTitle); + + context.runOnUiThread(new Runnable() + { + public void run() + { + context.loadingDialog.dismiss(); + } + }); + + return ret; + } + private int PowerOf2(int i) { int value = 1; diff --git a/project/java/googleplaygameservices/CloudSave.java b/project/java/googleplaygameservices/CloudSave.java index a6c40df3a..ae22aeadb 100644 --- a/project/java/googleplaygameservices/CloudSave.java +++ b/project/java/googleplaygameservices/CloudSave.java @@ -19,8 +19,24 @@ package com.google.example.games.basegameutils; import android.content.Intent; import android.os.Bundle; import android.util.Log; +import android.graphics.BitmapFactory; +import android.graphics.Bitmap; + +import java.io.*; import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.auth.GoogleAuthException; +import com.google.android.gms.auth.GoogleAuthUtil; +import com.google.android.gms.auth.UserRecoverableAuthException; +import com.google.android.gms.common.Scopes; +import com.google.android.gms.games.Games; +import com.google.android.gms.games.GamesStatusCodes; +import com.google.android.gms.games.snapshot.Snapshot; +import com.google.android.gms.games.snapshot.SnapshotMetadata; +import com.google.android.gms.games.snapshot.SnapshotMetadataBuffer; +import com.google.android.gms.games.snapshot.SnapshotMetadataChange; +import com.google.android.gms.games.snapshot.Snapshots; public class CloudSave implements GameHelper.GameHelperListener { @@ -40,54 +56,225 @@ public class CloudSave implements GameHelper.GameHelperListener { mHelper.setup(this); } - public GameHelper getGameHelper() { - return mHelper; - } - - public void onStart() { + public void onStart() + { mHelper.onStart(parent); } - public void onStop() { + public void onStop() + { mHelper.onStop(); } - public boolean save(String filename, String description, String imageFile) + public void onActivityResult(int request, int response, Intent data) { + mHelper.onActivityResult(request, response, data); + } + + public synchronized boolean save(String filename, String saveId, String dialogTitle, String description, String imageFile, long playedTimeMs) + { + Log.d("SDL", "CloudSave: save: file " + filename + " saveId " + saveId + " dialogTitle " + dialogTitle + " desc " + description + " imageFile " + imageFile + " playedTime " + playedTimeMs); + + if( !signIn() ) + return false; + + if( !filename.startsWith("/") ) + filename = Globals.DataDir + "/" + filename; + if( imageFile.length() > 0 && !imageFile.startsWith("/") ) + imageFile = Globals.DataDir + "/" + imageFile; + + try + { + if( saveId == null || saveId.length() == 0 ) + { + Log.i("SDL", "CloudSave: save: user dialog is not supported yet"); + return false; + } + + Snapshots.OpenSnapshotResult result = Games.Snapshots.open(getApiClient(), saveId, true).await(); + Snapshot crapshot = processSnapshotOpenResult(result, 0); + if( crapshot == null ) + return false; + + crapshot.writeBytes(readFile(filename)); + + Bitmap bmp = BitmapFactory.decodeFile(imageFile); + while( bmp != null && bmp.getByteCount() > Games.Snapshots.getMaxCoverImageSize(getApiClient()) ) + bmp = Bitmap.createScaledBitmap(bmp, bmp.getWidth() * 3 / 4, bmp.getHeight() * 3 / 4, true); + + SnapshotMetadataChange.Builder metadataChange = new SnapshotMetadataChange.Builder() + .setDescription(description) + .setPlayedTimeMillis(playedTimeMs); + if( bmp != null ) + metadataChange.setCoverImage(bmp); + + Games.Snapshots.commitAndClose(getApiClient(), crapshot, metadataChange.build()) + .setResultCallback(new ResultCallback() + { + public void onResult(Snapshots.CommitSnapshotResult r) + { + Log.i("SDL", "CloudSave: save final net sync result: " + r.getStatus().toString()); + } + }); + + Log.i("SDL", "CloudSave: save succeeded"); + return true; + } + catch(Exception e) + { + Log.i("SDL", "CloudSave: save failed: " + e.toString()); + } return false; } - public boolean load (String filename) + public synchronized boolean load(String filename, String saveId, String dialogTitle) { + Log.d("SDL", "CloudSave: load: file " + filename + " saveId " + saveId + " dialogTitle " + dialogTitle); + + if( !signIn() ) + return false; + + if( !filename.startsWith("/") ) + filename = Globals.DataDir + "/" + filename; + + try + { + if( saveId == null || saveId.length() == 0 ) + { + Log.i("SDL", "CloudSave: load: user dialog is not supported yet"); + return false; + } + + Snapshots.OpenSnapshotResult result = Games.Snapshots.open(getApiClient(), saveId, false).await(); + if (result.getStatus().getStatusCode() != GamesStatusCodes.STATUS_OK) + { + Log.i("SDL", "CloudSave: load: failed to load game " + saveId + ": " + result.getStatus()); + return false; + } + + boolean written = writeFile(filename, result.getSnapshot().readFully()); + Log.i("SDL", "CloudSave: load: status: " + written); + return written; + } + catch(Exception e) + { + Log.i("SDL", "CloudSave: load failed: " + e.toString()); + } return false; } - public class loadDialogResult - { - public boolean status = false; - public String filename = ""; - } - - public loadDialogResult loadDialog(String filename, String dialogTitle) - { - loadDialogResult res = new loadDialogResult(); - res.status = false; - res.filename = ""; - return res; - } - // ===== Private API ===== - public void onActivityResult(int request, int response, Intent data) { - mHelper.onActivityResult(request, response, data); + boolean signInSucceeded = false; + boolean signInFailed = false; + public boolean signIn() + { + //Log.i("SDL", "CloudSave: signIn()"); + if( !isSignedIn() ) + { + signInSucceeded = false; + signInFailed = false; + Log.i("SDL", "CloudSave: beginUserInitiatedSignIn()"); + beginUserInitiatedSignIn(); + Log.i("SDL", "CloudSave: beginUserInitiatedSignIn() exit"); + while (!signInSucceeded && !signInFailed) + { + try { Thread.sleep(300); } catch( Exception e ) {} + } + return signInSucceeded; + } + return true; } + public void onSignInSucceeded() { Log.i("SDL", "CloudSave: onSignInSucceeded()"); + signInSucceeded = true; } public void onSignInFailed() { Log.i("SDL", "CloudSave: onSignInFailed()"); + signInFailed = true; + } + + public Snapshot processSnapshotOpenResult(Snapshots.OpenSnapshotResult result, int retryCount) + { + Snapshot mResolvedSnapshot = null; + retryCount++; + int status = result.getStatus().getStatusCode(); + + Log.i("SDL", "CloudSave: processSnapshotOpenResult status: " + result.getStatus()); + + if (status == GamesStatusCodes.STATUS_OK) { + return result.getSnapshot(); + } else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONTENTS_UNAVAILABLE) { + return result.getSnapshot(); + } else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONFLICT) { + Snapshot snapshot = result.getSnapshot(); + Snapshot conflictSnapshot = result.getConflictingSnapshot(); + + // Resolve between conflicts by selecting the newest of the conflicting snapshots. + mResolvedSnapshot = snapshot; + + if (snapshot.getMetadata().getPlayedTime() == conflictSnapshot.getMetadata().getPlayedTime()) { + if (snapshot.getMetadata().getLastModifiedTimestamp() < conflictSnapshot.getMetadata().getLastModifiedTimestamp()) { + mResolvedSnapshot = conflictSnapshot; + } + } else if (snapshot.getMetadata().getPlayedTime() < conflictSnapshot.getMetadata().getPlayedTime()) { + mResolvedSnapshot = conflictSnapshot; + } + + Snapshots.OpenSnapshotResult resolveResult = Games.Snapshots.resolveConflict( + getApiClient(), result.getConflictId(), mResolvedSnapshot) + .await(); + + if (retryCount < 3) { + return processSnapshotOpenResult(resolveResult, retryCount); + } else { + Log.i("SDL", "CloudSave: could not resolve snapshot conflict"); + } + } + Log.i("SDL", "CloudSave: could not get savegame snapshot"); + return null; + } + + static public byte[] readFile(String filename) + { + int len = (int)(new File(filename).length()); + if( len == 0 ) + return new byte[0]; + try + { + byte buf[] = new byte[len]; + if( new FileInputStream(filename).read(buf, 0, len) != len ) + return new byte[0]; + return buf; + } + catch( Exception e ) + { + Log.i("SDL", "CloudSave: readFile() error: " + e.toString()); + } + return new byte[0]; + } + + static public boolean writeFile(String filename, byte[] data) + { + try + { + BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(filename)); + out.write(data, 0, data.length); + out.close(); + return true; + } + catch( Exception e ) + { + Log.i("SDL", "CloudSave: writeFile() error: " + e.toString() + " file " + filename); + } + return false; + } + + public GameHelper getGameHelper() { + return mHelper; } public GoogleApiClient getApiClient() { diff --git a/project/java/translations/generate.sh b/project/java/translations/generate.sh index f2f8911f7..f3dc560ba 100755 --- a/project/java/translations/generate.sh +++ b/project/java/translations/generate.sh @@ -2,6 +2,8 @@ grep '\([^<]*\).*/\1/'` if [ "$var" = "app_name" ]; then diff --git a/project/java/translations/values-fr/strings.xml b/project/java/translations/values-fr/strings.xml index de80ea809..750e2c023 100644 --- a/project/java/translations/values-fr/strings.xml +++ b/project/java/translations/values-fr/strings.xml @@ -188,4 +188,5 @@ The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information. License check failed. Unknown error. +Accessing network, please wait diff --git a/project/java/translations/values-ru/strings.xml b/project/java/translations/values-ru/strings.xml index b699eafe9..1d11a6560 100644 --- a/project/java/translations/values-ru/strings.xml +++ b/project/java/translations/values-ru/strings.xml @@ -161,4 +161,5 @@ The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information. License check failed. Unknown error. +Accessing network, please wait diff --git a/project/java/translations/values-uk/strings.xml b/project/java/translations/values-uk/strings.xml index b9f5456fb..291ed382e 100644 --- a/project/java/translations/values-uk/strings.xml +++ b/project/java/translations/values-uk/strings.xml @@ -160,4 +160,5 @@ The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information. License check failed. Unknown error. +Accessing network, please wait diff --git a/project/java/translations/values/strings.xml b/project/java/translations/values/strings.xml index 6d03455e6..1d81b5ccc 100644 --- a/project/java/translations/values/strings.xml +++ b/project/java/translations/values/strings.xml @@ -192,6 +192,8 @@ The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information. License check failed. Unknown error. + Accessing network, please wait + ==GOOGLEPLAYGAMESERVICES_APP_ID== diff --git a/project/jni/SettingsTemplate.mk b/project/jni/SettingsTemplate.mk index 6e956704a..5eb0c4040 100644 --- a/project/jni/SettingsTemplate.mk +++ b/project/jni/SettingsTemplate.mk @@ -3,7 +3,7 @@ APP_MODULES := application sdl-1.2 sdl_main sdl_native_helpers jpeg png ogg flac vorbis freetype tremor ogg # To filter out static libs from all libs in makefile -APP_AVAILABLE_STATIC_LIBS := jpeg png tremor freetype xerces ogg tremor vorbis flac boost_date_time boost_filesystem boost_iostreams boost_program_options boost_regex boost_signals boost_system boost_thread boost_locale glu icudata icutest icui18n icuio icule iculx icutu icuuc +APP_AVAILABLE_STATIC_LIBS := jpeg png tremor freetype xerces ogg tremor vorbis flac boost_date_time boost_filesystem boost_iostreams boost_program_options boost_regex boost_signals boost_system boost_thread boost_locale glu icudata icutest icui18n icuio icule iculx icutu icuuc sdl_savepng APP_ABI := armeabi diff --git a/project/jni/application/biniax2/src/biniax.c b/project/jni/application/biniax2/src/biniax.c index 6f2d9f5e7..8f4b70298 100644 --- a/project/jni/application/biniax2/src/biniax.c +++ b/project/jni/application/biniax2/src/biniax.c @@ -1251,7 +1251,8 @@ BNX_BOOL saveGame( BNX_GAME *game ) sysFPut16( game->level_count, file ); fclose( file ); - SDL_ANDROID_CloudSave( sysGetFullFileName( csSaveGameName ) ); + SDL_SaveBMP(SDL_GetVideoSurface(), "screenshot.bmp"); + SDL_ANDROID_CloudSave( sysGetFullFileName( csSaveGameName ), "save", "Biniax2", "savegame", "screenshot.bmp", game->moves ); return BNX_TRUE; } @@ -1262,7 +1263,7 @@ BNX_BOOL loadGame( BNX_GAME *game ) BNX_INT32 i; BNX_INT32 j; - SDL_ANDROID_CloudLoad( sysGetFullFileName( csSaveGameName ) ); + SDL_ANDROID_CloudLoad( sysGetFullFileName( csSaveGameName ), "save", "Biniax2" ); if ( sysGetFileLen( sysGetFullFileName( csSaveGameName ) ) != cSaveFileSize ) return BNX_FALSE; diff --git a/project/jni/sdl-1.2/include/SDL_android.h b/project/jni/sdl-1.2/include/SDL_android.h index 21afb507e..0c4cf02c3 100644 --- a/project/jni/sdl-1.2/include/SDL_android.h +++ b/project/jni/sdl-1.2/include/SDL_android.h @@ -96,26 +96,27 @@ extern DECLSPEC void SDLCALL SDL_ANDROID_CloseAudioRecording(void); /* Save the file to the cloud, filename must be already present on disk. This function will block, until user signs in to the cloud account, and presses Save button. -Description and imageFile may be NULL. +If saveId is NULL or empty, user will select the savegame from the dialog, if it's not empty - +do not show any dialog to the user, except foir sign-in, and write the savegame with specified ID. +dialogTitle may be NULL. +description and screenshotFile may be NULL. playedTimeMs is used for conflict resolution - +savegame with longer play time will get priority. +Use SDL_SaveBMP(SDL_GetVideoSurface(), "screenshot.bmp") to create the screenshot. Returns 1 if save succeeded, 0 if user aborted sign-in, or there was no network available. */ -extern DECLSPEC int SDLCALL SDL_ANDROID_CloudSave(const char * filename, const char * description, const char * imageFile); +extern DECLSPEC int SDLCALL SDL_ANDROID_CloudSave(const char *filename, const char *saveId, const char *dialogTitle, + const char *description, const char *screenshotFile, long long playedTimeMs); /* Load the specified file from the cloud. -This function will block, until user signs in to the cloud account. -Returns 1 if load succeeded, 0 if user aborted sign-in, or there was no network available. -*/ -extern DECLSPEC int SDLCALL SDL_ANDROID_CloudLoad(const char *filename); - -/* -Show the file loading dialog, to allow user to pick up a file from the cloud. -This function will block, until user signs in to the cloud account, and selects a file. -The resulting filename is written to the filename buffer, which must be 512 bytes or more. +This function will block, until user signs in to the cloud account, and selects a savegame. +The resulting savegame is written to the passed filename. +If saveId is NULL or empty, user will select the savegame from the dialog, if it's not empty - +do not show any dialog to the user, except foir sign-in, and read the savegame with specified ID. dialogTitle may be NULL. Returns 1 if load succeeded, 0 if user aborted sign-in, or there was no network available. */ -extern DECLSPEC int SDLCALL SDL_ANDROID_CloudLoadDialog(char *filename, int len, const char *dialogTitle); +extern DECLSPEC int SDLCALL SDL_ANDROID_CloudLoad(const char *filename, const char *saveId, const char *dialogTitle); #ifdef __cplusplus } diff --git a/project/jni/sdl-1.2/src/video/android/SDL_androidvideo.c b/project/jni/sdl-1.2/src/video/android/SDL_androidvideo.c index d55f6d114..1dcd934e3 100644 --- a/project/jni/sdl-1.2/src/video/android/SDL_androidvideo.c +++ b/project/jni/sdl-1.2/src/video/android/SDL_androidvideo.c @@ -80,6 +80,8 @@ static jmethodID JavaGetAdvertisementParams = NULL; static jmethodID JavaSetAdvertisementVisible = NULL; static jmethodID JavaSetAdvertisementPosition = NULL; static jmethodID JavaRequestNewAdvertisement = NULL; +static jmethodID JavaRequestCloudSave = NULL; +static jmethodID JavaRequestCloudLoad = NULL; static int glContextLost = 0; static int showScreenKeyboardDeferred = 0; static const char * showScreenKeyboardOldText = ""; @@ -347,6 +349,11 @@ JAVA_EXPORT_NAME(DemoRenderer_nativeInitJavaCallbacks) ( JNIEnv* env, jobject t JavaSetAdvertisementVisible = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "setAdvertisementVisible", "(I)V"); JavaSetAdvertisementPosition = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "setAdvertisementPosition", "(II)V"); JavaRequestNewAdvertisement = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "requestNewAdvertisement", "()V"); + + JavaRequestCloudSave = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "cloudSave", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)Z"); + JavaRequestCloudLoad = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "cloudLoad", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z"); ANDROID_InitOSKeymap(); } @@ -467,20 +474,55 @@ int SDLCALL SDL_ANDROID_RequestNewAdvertisement(void) return 1; } -int SDLCALL SDL_ANDROID_CloudSave(const char * filename, const char * description, const char * imageFile) +int SDLCALL SDL_ANDROID_CloudSave(const char *filename, const char *saveId, const char *dialogTitle, + const char *description, const char *screenshotFile, long long playedTimeMs) { - return 0; + if( !filename ) + return 0; + if( !saveId ) + saveId = ""; + if( !dialogTitle ) + dialogTitle = ""; + if( !description ) + description = ""; + if( !screenshotFile ) + screenshotFile = ""; + (*JavaEnv)->PushLocalFrame(JavaEnv, 5); + jstring s1 = (*JavaEnv)->NewStringUTF(JavaEnv, filename); + jstring s2 = (*JavaEnv)->NewStringUTF(JavaEnv, saveId); + jstring s3 = (*JavaEnv)->NewStringUTF(JavaEnv, dialogTitle); + jstring s4 = (*JavaEnv)->NewStringUTF(JavaEnv, description); + jstring s5 = (*JavaEnv)->NewStringUTF(JavaEnv, screenshotFile); + int result = (*JavaEnv)->CallBooleanMethod( JavaEnv, JavaRenderer, JavaRequestCloudSave, s1, s2, s3, s4, s5, playedTimeMs ); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s5); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s4); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s3); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s2); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s1); + (*JavaEnv)->PopLocalFrame(JavaEnv, NULL); + return result; } -int SDLCALL SDL_ANDROID_CloudLoad(const char *filename) +int SDLCALL SDL_ANDROID_CloudLoad(const char *filename, const char *saveId, const char *dialogTitle) { - return 0; + if( !filename ) + return 0; + if( !saveId ) + saveId = ""; + if( !dialogTitle ) + dialogTitle = ""; + (*JavaEnv)->PushLocalFrame(JavaEnv, 3); + jstring s1 = (*JavaEnv)->NewStringUTF(JavaEnv, filename); + jstring s2 = (*JavaEnv)->NewStringUTF(JavaEnv, saveId); + jstring s3 = (*JavaEnv)->NewStringUTF(JavaEnv, dialogTitle); + int result = (*JavaEnv)->CallBooleanMethod( JavaEnv, JavaRenderer, JavaRequestCloudLoad, s1, s2, s3 ); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s3); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s2); + (*JavaEnv)->DeleteLocalRef(JavaEnv, s1); + (*JavaEnv)->PopLocalFrame(JavaEnv, NULL); + return result; } -int SDLCALL SDL_ANDROID_CloudLoadDialog(char *filename, int len, const char *dialogTitle) -{ - return 0; -} // Dummy callback for SDL2 to satisfy linker extern void SDL_Android_Init(JNIEnv* env, jclass cls); diff --git a/project/jni/sdl_savepng/Android.mk b/project/jni/sdl_savepng/Android.mk new file mode 100644 index 000000000..22d050355 --- /dev/null +++ b/project/jni/sdl_savepng/Android.mk @@ -0,0 +1,19 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := sdl_savepng + +LOCAL_C_INCLUDES := $(LOCAL_PATH) $(LOCAL_PATH)/../png/include $(LOCAL_PATH)/../sdl-$(SDL_VERSION)/include $(LOCAL_PATH)/include + +LOCAL_CPP_EXTENSION := .cpp + +LOCAL_SRC_FILES := $(notdir $(wildcard $(LOCAL_PATH)/*.c)) + +LOCAL_STATIC_LIBRARIES := png + +LOCAL_SHARED_LIBRARIES := sdl-$(SDL_VERSION) + +LOCAL_LDLIBS := -lz + +include $(BUILD_STATIC_LIBRARY) diff --git a/project/jni/sdl_savepng/README.md b/project/jni/sdl_savepng/README.md new file mode 100644 index 000000000..cf5b0f2ef --- /dev/null +++ b/project/jni/sdl_savepng/README.md @@ -0,0 +1,101 @@ +# SDL_SavePNG + +Minimal libpng interface to save SDL_Surfaces as PNG files. + +You might want to take a look in "savepng.h" - it is much shorter and simpler +than this README. + +## Install + +Add "savepng.c" and "savepng.h" to your project. + +Link the libpng library, i.e. add the `-lpng` LDFLAG (even if you already have +`-lSDL_image`). + +## Use + +``` +#include "savepng.h" + +SDL_Surface *bmp = ... //your surface +if (SDL_SavePNG(bmp, "image.png")) { //boring way with error checking + printf("Unable to save png -- %s\n", SDL_GetError()); +} +``` + +As you can see, `SDL_SavePNG` accepts an SDL_Surface and a filename for it's +input. Similar to SDL_SaveBMP, it is a wrapper around the actual RWops-based +`SDL_SavePNG_RW` function, so you could use that, if needed. + +Lastly, there is `SDL_PNGFormatAlpha`, modeled after SDL_DisplayFormatAlpha, +that would convert *any SDL_Surface* to an *SDL_Surface suitable for PNG +output*. Each call to `SDL_PNGFormatAlpha` produces a **new** SDL_Surface that +**must** be freed using `SDL_FreeSurface`. + +``` +//safest way, usefull for 'screen' surface +SDL_Surface *tmp = SDL_PNGFormatAlpha(screen); +SDL_SavePNG(tmp, "screenshot.png"); +SDL_FreeSurface(tmp) +``` + +Such conversion is actually only required for *one* surface format (see below), +and would do **nothing** for all other formats, making it **very fast**. The +format in question is: + +### 32-bpp surfaces without alpha + +There is a interesting caveat of combining naive libpng and cunning SDL in a +32-bpp video mode. + +The *screen* surface (obtained by `SDL_SetVideoMode` or similarly) might (and +will!) ignore it's alpha-component even in the 32bpp mode. Meaning that an +0xAARRGGBB color would be blitted as 0xFFrrggbb irregardless, as if it was a +24bpp color. + +Since screen itself is never blitted onto anything else, ignoring the alpha +makes perfect sense. However, unlike 24bpp images, the alpha component *does* +exist. Thus, when such surface is saved, it appears to be completely +transparent, as the alpha values for each pixel are set to 0. + +Depending on your video mode, you might or might not need to first convert your +surface using `SDL_PNGFormatAlpha`. If you have absolute control over the video +surface, you can force it to 24bpp (or less) mode, which would avoid the +problem. + +If the surface passed to `SDL_PNGFormatAlpha` is already suitable, a no-op is +performed. It is very fast, so you should probably always convert your surfaces +before saving. + +### No text chunks + +Unfortunately, a simplistic interface such as SDL_SavePNG provides no means to +write PNG meta-data. If you need to add iTXT chunks to your PNGs, you would +have to modify this code or write your own version. + +If you have some kind of simple API, that would be thematically consistent with +SDL, in mind -- please share. + +## Demo + +See `main.c` and `Makefile` for an example program. It too is shorter than this +README. + +# About + +The problem in question is very simple, and this little piece of functionality +was implemented and re-implemented multiple times by multiple authors (notably, +Angelo "Encelo" Theodorou and Darren Grant, among others). I decided to write +my own version to ensure it's correctness, learn more about libpng, and to +provide a copy-pastable, maintained, libpng15-aware, palette-supporting +variation that I could link to. You can view it as a continuation of their +efforts. + +SDL_Image would've been perfect place for this, but that library has different +purposes. + +*Next up: code to load SDL_Surfaces as OpenGL 1.1 textures. J/K ;)* + +# Copying + +SDL_SavePNG is available under the zlib/libpng license. \ No newline at end of file diff --git a/project/jni/sdl_savepng/include/savepng.h b/project/jni/sdl_savepng/include/savepng.h new file mode 100644 index 000000000..1f97e32c3 --- /dev/null +++ b/project/jni/sdl_savepng/include/savepng.h @@ -0,0 +1,36 @@ +#ifndef _SDL_SAVEPNG +#define _SDL_SAVEPNG +/* + * SDL_SavePNG -- libpng-based SDL_Surface writer. + * + * This code is free software, available under zlib/libpng license. + * http://www.libpng.org/pub/png/src/libpng-LICENSE.txt + */ +#include +/* + * Save an SDL_Surface as a PNG file. + * + * Returns 0 success or -1 on failure, the error message is then retrievable + * via SDL_GetError(). + */ +#define SDL_SavePNG(surface, file) \ + SDL_SavePNG_RW(surface, SDL_RWFromFile(file, "wb"), 1) + +/* + * Save an SDL_Surface as a PNG file, using writable RWops. + * + * surface - the SDL_Surface structure containing the image to be saved + * dst - a data stream to save to + * freedst - non-zero to close the stream after being written + * + * Returns 0 success or -1 on failure, the error message is then retrievable + * via SDL_GetError(). + */ +extern int SDL_SavePNG_RW(SDL_Surface *surface, SDL_RWops *rw, int freedst); + +/* + * Return new SDL_Surface with a format suitable for PNG output. + */ +extern SDL_Surface *SDL_PNGFormatAlpha(SDL_Surface *src); + +#endif diff --git a/project/jni/sdl_savepng/savepng.c b/project/jni/sdl_savepng/savepng.c new file mode 100644 index 000000000..1076f13cf --- /dev/null +++ b/project/jni/sdl_savepng/savepng.c @@ -0,0 +1,155 @@ +/* + * SDL_SavePNG -- libpng-based SDL_Surface writer. + * + * This code is free software, available under zlib/libpng license. + * http://www.libpng.org/pub/png/src/libpng-LICENSE.txt + */ +#include +#include + +#define SUCCESS 0 +#define ERROR -1 + +#define USE_ROW_POINTERS + +#if SDL_BYTEORDER == SDL_BIG_ENDIAN +#define rmask 0xFF000000 +#define gmask 0x00FF0000 +#define bmask 0x0000FF00 +#define amask 0x000000FF +#else +#define rmask 0x000000FF +#define gmask 0x0000FF00 +#define bmask 0x00FF0000 +#define amask 0xFF000000 +#endif + +/* libpng callbacks */ +static void png_error_SDL(png_structp ctx, png_const_charp str) +{ + SDL_SetError("libpng: %s\n", str); +} +static void png_write_SDL(png_structp png_ptr, png_bytep data, png_size_t length) +{ + SDL_RWops *rw = (SDL_RWops*)png_get_io_ptr(png_ptr); + SDL_RWwrite(rw, data, sizeof(png_byte), length); +} + +SDL_Surface *SDL_PNGFormatAlpha(SDL_Surface *src) +{ + SDL_Surface *surf; + SDL_Rect rect = { 0 }; + + /* NO-OP for images < 32bpp and 32bpp images that already have Alpha channel */ + if (src->format->BitsPerPixel <= 24 || src->format->Amask) { + src->refcount++; + return src; + } + + /* Convert 32bpp alpha-less image to 24bpp alpha-less image */ + rect.w = src->w; + rect.h = src->h; + surf = SDL_CreateRGBSurface(src->flags, src->w, src->h, 24, + src->format->Rmask, src->format->Gmask, src->format->Bmask, 0); + SDL_LowerBlit(src, &rect, surf, &rect); + + return surf; +} + +int SDL_SavePNG_RW(SDL_Surface *surface, SDL_RWops *dst, int freedst) +{ + png_structp png_ptr; + png_infop info_ptr; + png_colorp pal_ptr; + SDL_Palette *pal; + int i, colortype; +#ifdef USE_ROW_POINTERS + png_bytep *row_pointers; +#endif + /* Initialize and do basic error checking */ + if (!dst) + { + SDL_SetError("Argument 2 to SDL_SavePNG_RW can't be NULL, expecting SDL_RWops*\n"); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + if (!surface) + { + SDL_SetError("Argument 1 to SDL_SavePNG_RW can't be NULL, expecting SDL_Surface*\n"); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, png_error_SDL, NULL); /* err_ptr, err_fn, warn_fn */ + if (!png_ptr) + { + SDL_SetError("Unable to png_create_write_struct on %s\n", PNG_LIBPNG_VER_STRING); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) + { + SDL_SetError("Unable to png_create_info_struct\n"); + png_destroy_write_struct(&png_ptr, NULL); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + if (setjmp(png_jmpbuf(png_ptr))) /* All other errors, see also "png_error_SDL" */ + { + png_destroy_write_struct(&png_ptr, &info_ptr); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + + /* Setup our RWops writer */ + png_set_write_fn(png_ptr, dst, png_write_SDL, NULL); /* w_ptr, write_fn, flush_fn */ + + /* Prepare chunks */ + colortype = PNG_COLOR_MASK_COLOR; + if (surface->format->BytesPerPixel > 0 + && surface->format->BytesPerPixel <= 8 + && (pal = surface->format->palette)) + { + colortype |= PNG_COLOR_MASK_PALETTE; + pal_ptr = (png_colorp)malloc(pal->ncolors * sizeof(png_color)); + for (i = 0; i < pal->ncolors; i++) { + pal_ptr[i].red = pal->colors[i].r; + pal_ptr[i].green = pal->colors[i].g; + pal_ptr[i].blue = pal->colors[i].b; + } + png_set_PLTE(png_ptr, info_ptr, pal_ptr, pal->ncolors); + free(pal_ptr); + } + else if (surface->format->BytesPerPixel > 3 || surface->format->Amask) + colortype |= PNG_COLOR_MASK_ALPHA; + + png_set_IHDR(png_ptr, info_ptr, surface->w, surface->h, 8, colortype, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + +// png_set_packing(png_ptr); + + /* Allow BGR surfaces */ + if (surface->format->Rmask == bmask + && surface->format->Gmask == gmask + && surface->format->Bmask == rmask) + png_set_bgr(png_ptr); + + /* Write everything */ + png_write_info(png_ptr, info_ptr); +#ifdef USE_ROW_POINTERS + row_pointers = (png_bytep*) malloc(sizeof(png_bytep)*surface->h); + for (i = 0; i < surface->h; i++) + row_pointers[i] = (png_bytep)(Uint8*)surface->pixels + i * surface->pitch; + png_write_image(png_ptr, row_pointers); + free(row_pointers); +#else + for (i = 0; i < surface->h; i++) + png_write_row(png_ptr, (png_bytep)(Uint8*)surface->pixels + i * surface->pitch); +#endif + png_write_end(png_ptr, info_ptr); + + /* Done */ + png_destroy_write_struct(&png_ptr, &info_ptr); + if (freedst) SDL_RWclose(dst); + return (SUCCESS); +} diff --git a/project/proguard.cfg b/project/proguard.cfg index 7bf261def..943385335 100644 --- a/project/proguard.cfg +++ b/project/proguard.cfg @@ -1,4 +1,4 @@ --optimizationpasses 1 +-optimizationpasses 0 -dontusemixedcaseclassnames -dontskipnonpubliclibraryclasses -dontpreverify