Shared Project: Android app and iOS app sample apps

In the github sample project

I’m building an iOS app and Android app that shares core logic through a Silver Shared Project (Swift).

This project originally was intended to port a Promise/Future programming style to Swift, but now is evolved through the amazing RemObjects Fire IDE project pretty much like a shared library to build cross platforms core logic libraries (Http, I/O, File System, Database).

The Fire project structure has the following structure

so you can find in the repo:

  • A Shared Project in Silver / Swift code (2.1 supported on latest Fire Beta 8.3.92)
  • A iOS target, to build a static ARMv7/ARM64 library
  • A Android target to build Android-19 jars.
  • A (Xcode project) iOS Sample app (CrossTest). This is intended like a playground for Swift code and ObjC bridging as well as the iOS app that shares the static library;
  • A Android Sample app (AndroidStudio project), to test the shared jar libraries build in Swift.

I would like to share this solution since some achievements were done until now:

So far I was able to

  • Perform HTTP calls in both iOS and Android targets;
  • Create a Database through Sugar.Data iOS target;
  • The Promise runs only in Xcode/Swift 2.1. It will be able to run in Fire very soon (See previous note).

Todos:

  • Database support in Android target through Sugar.Data (See issues below).

Issues:

  • Currently I’m facing some issues on Sugar.Data / Android target:

    01-13 11:50:02.277 16599-16621/? E/AndroidRuntime: FATAL EXCEPTION: AsyncTask #1
    Process: musixmatch.com.sampleandroidapp, PID: 16599
    java.lang.RuntimeException: An error occurred while executing doInBackground()
    at android.os.AsyncTask$3.done(AsyncTask.java:309)
    at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:354)
    at java.util.concurrent.FutureTask.setException(FutureTask.java:223)
    at java.util.concurrent.FutureTask.run(FutureTask.java:242)
    at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:234)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    at java.lang.Thread.run(Thread.java:818)
    Caused by: sugar.SugarAppContextMissingException: Environment.ApplicationContext is not set.
    at samplelibrary.SharedClassTest.databaseSetup(/Volumes/MacHDD2/Developmemt/ParisiLabs/swift-promise-example/StaticLibrary/SharedProject/Library.swift:13)
    at musixmatch.com.sampleandroidapp.MainActivity$BrowserTask.doInBackground(MainActivity.java:82)
    at musixmatch.com.sampleandroidapp.MainActivity$BrowserTask.doInBackground(MainActivity.java:66)
    at android.os.AsyncTask$2.call(AsyncTask.java:295)
    at java.util.concurrent.FutureTask.run(FutureTask.java:237)
    at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:234)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    at java.lang.Thread.run(Thread.java:818)
    01-13 11:50:02.280 588-646/? W/ActivityManager: Force finishing activity musixmatch.com.sampleandroidapp/.MainActivity

A sugar.SugarAppContextMissingException is raised when trying to access the file system with description

Environment.ApplicationContext is not set.

  • Filesystem access. Not properly an issue, but when accessing the iOS bundle folders in Cocoa normally you access the /Documents or /Cache folders to write data:

Optional("file:///var/mobile/Containers/Data/Application/AE3D89D3-7D56-49C5-8470-0CF3BDEB2146/Documents/testdb.sql")

While using Sugar.IO I was able to access the /Application\ Support folder:

/var/mobile/Containers/Data/Application/AE3D89D3-7D56-49C5-8470-0CF3BDEB2146/Library/Application Support//db.sql

through the snippet

                // file system folder path
		let Separator:Char=Sugar.io.folder.Separator;
		let userLocal:Folder=Sugar.IO.Folder.UserLocal();
		let userLocalPath:String=userLocal.Path;
		
		let dbPath:String = Sugar.io.Path.Combine(userLocalPath,Separator+"db.sql");
		
		writeLn (dbPath )

But I guess there is a way to address the same folders i.e.Documents directory like in Cocoa:

let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)

(due to the previous crash I’m not dealing with the android app container yet for the db location).

Any help on this project is welcome!

A note for developers:
As recommended by RemObject team, the shared library should be used in Android and iOS apps Fire project, rather that in a Xcode toolchain or AndroidStudio, anyways my aim here was to have those IDE to evaluate existing libraries, projects etc.

1 Like

I fixed the issue of the SugarAppContextMissingException (see docs at http://docs.elementscompiler.com/API/Sugar/SugarAppContextMissingException_Class/)

This was due to the lacks of the Android Context android.content.Context backed on Sugar from the Android app, so I did in the SharedProject

#if java
public func context(var context:android.content.Context) ->() {
	Sugar.Environment.ApplicationContext = context;
}
#endif

and then in the Android app:

            SharedClassTest sampleAPI = new SharedClassTest();
            sampleAPI.context( getApplicationContext() );

So I have the database working on the Android app as well with Sugar.Data:

The file system path was

01-13 17:10:04.221 27741-27758/? I/System.out: /data/user/0/musixmatch.com.sampleandroidapp/files//db.sql

and the SQLiteConnectionPool complains something, so it’s there:

01-13 17:10:04.633 27741-27749/? W/SQLiteConnectionPool: A SQLiteConnection object for database '/data/user/0/musixmatch.com.sampleandroidapp/files//db.sql' was leaked! Please fix your application to end transactions in progress properly and to close the database when it is no longer needed.

Some advances

  • In order to find the Documents folder on iOS app rather than using the Application/ \Support folder:

     // scan app sub-folders
     let userLocal:Folder=Sugar.IO.Folder.UserLocal()
     let parentPath:String=Path.GetParentDirectory( Path.GetParentDirectory(userLocal.Path) );
     writeLn ( "Scanning \(parentPath)..." )
     let folders:String[]=Sugar.io.FolderUtils.GetFolders(parentPath,false);
     for f in folders {
        if f.EndsWith("Documents") {
           writeLn("Documents folder \(f)");
        }
     }
    

.

  • According to SQLiteQueryResult handling explained in Sugar.Data: Handling SQLiteConnection, SQLiteQueryResult, SQLiteException in Swift/Silver
    to handle a ResultSet cursor:

                      do {
     			
                             // insert example
     			let INSERT = "INSERT OR REPLACE INTO CACHE (cache_key, cache_value, timestamp) VALUES (?,?,?);";
     			conn.Execute(INSERT,["KEY","PIPPO","20150101"]);
    
     			// select example
     			let SELECT = "SELECT * from CACHE"
     			let result:SQLiteQueryResult=conn.ExecuteQuery(SELECT);
     			while result.MoveNext() {
     				writeLn( result.GetString( 0 ) ); // col1
     				writeLn( result.GetString( 1 ) ); // col2
     				writeLn( result.GetString( 2 ) ); // col3
     			}
     		} catch let error as SQLiteException {
     			writeLn("sql error");
     			writeLn( error.description.ToString() );
     		}

Thanks to @ck the SQL issues were fixed (Cocoa). On Android I’m now facing issues to figure out the UserLocal path where to put the applications files, I’m doing:

                let Separator:Char=Sugar.io.folder.Separator;
		let userLocal:Folder=Sugar.IO.Folder.UserLocal();
		let userLocalPath:String=userLocal.Path;
		let dbPath:String = Sugar.io.Path.Combine(userLocalPath,"db")
		let dbFilePath:String = Sugar.io.Path.Combine(dbPath,"db.sql")
		
		let altFilePath:String = Sugar.io.Path.Combine(userLocalPath,"db.sql")
		
		writeLn("User path \(userLocalPath)");
		writeLn("App folder path \(dbPath)");
		writeLn("Database path \(dbFilePath) \(altFilePath)");

and this prints out:

01-14 12:02:43.392 15731-15745/? I/System.out: User path /
01-14 12:02:43.392 15731-15745/? I/System.out: App folder path /db
01-14 12:02:43.392 15731-15745/? I/System.out: Database path /db/db.sql /db.sql

so it seems that the Sugar.IO.Folder.UserLocal() gives back the system root folder / that is not writable. In fact I came out with an error when trying to write it down:

do {
				Sugar.IO.FolderUtils.Create(dbPath);
//...

and then

01-14 12:02:43.405 15731-15745/? I/System.out: sugar.SugarIOException: Unable to create folder /db

yeah that makes sense (db not being writable);

you’ll want one of these:
http://developer.android.com/guide/topics/data/data-storage.html

which I don’t think are exposed in Sugar.

right, but look at the logs I have posted above, for some weird reason, yesterday I was able to create the database file on Android app:

01-13 17:10:04.221 27741-27758/? I/System.out: /data/user/0/musixmatch.com.sampleandroidapp/files//db.sql

and to run as well:

01-13 17:10:04.633 27741-27749/? W/SQLiteConnectionPool: A SQLiteConnection object for database '/data/user/0/musixmatch.com.sampleandroidapp/files//db.sql' was leaked! Please fix your application to end transactions in progress properly and to close the database when it is no longer needed.

and the code was quite like the same.

Btw, as you have said, I think at least there is need to have in Sugar for cross-platform fs access on Android:

for the Internal Storage. External Storage could be a nice to have.

@ck Looking at Sugar.IO sources - https://github.com/remobjects/sugar/blob/develop/Sugar/IO/Folder.pas

I can see that

class method Folder.UserLocal: Folder;
begin
  {$IF ANDROID}
  SugarAppContextMissingException.RaiseIfMissing;
  exit Environment.ApplicationContext.FilesDir;
  {$ELSE}
  exit new java.io.File(System.getProperty("user.home"));
  {$ENDIF}
end; 

the Sugar.IO.Folder.UserLocal was supposed to be the Environment.ApplicationContext.FilesDir, isn’t?

@ck I thought was a problem of the API

Sugar.IO.FolderUtils.Create(dbPath);

so I tried to use the “.” path, but actually the error is the root path again:

01-15 12:39:39.471 2459-2459/? I/System.out: java.sql.SQLException: opening db: '/db.sql': open failed: EROFS (Read-only file system)

Note: A note for developers, I add to add the dependency org.xerial:sqlite-jdbc:3.8.11.2 to run the Android App / AndroidStudio. that runs the Shared Library (that comes with Sugar.Data within).

Same as the other thread, i think there’s a mismatch between the cooper & android jar.

@ck ok so this seems to be a bug in Fire 8.3.92 - beta according to Sugar.Data and SQlite JDBC Driver on Android App

Any way to have a solution?

There’s currently no solution to open and work with these Oxygene projects in Fire without an Oxygene license, no. That said you don’t need a license to just build them. Simply open the solution, ignore the license messages, and it “build” — it’ll build fine. The missing license just prevents you from working with Oxygene projects, not compiling them.

@mh I see, I think the problem is that when Fire (the latest beta 9.3.92.1917 - thank you for that) does the build of Sugar (latest commit 8f62c6a10098eb162023aaf8b2df38261a7d1c69), and then look at the files generated in the bin output folder bin/Android

macbookproloreto:bin admin$ ls -l
total 0
drwxr-xr-x  3 admin  staff  102 19 Gen 15:02 Android
drwxr-xr-x  4 admin  staff  136 19 Gen 15:02 Java
drwxr-xr-x  4 admin  staff  136 15 Dic 12:31 OS X
drwxr-xr-x  4 admin  staff  136 15 Dic 12:31 iOS
drwxr-xr-x  4 admin  staff  136 15 Dic 12:31 tvOS

path: /Volumes/MacHDD2/Developmemt/Java/sugar/Sugar/bin/Android

and bin/Android from

macbookproloreto:bin admin$ ls -l
total 11576
drwxr-xr-x  3 admin  staff      102 19 Gen 15:02 Android
drwxr-xr-x  5 admin  staff      170 19 Gen 15:02 Java
drwxr-xr-x  4 admin  staff      136 15 Dic 12:56 OS X
-rw-r--r--  1 admin  staff  1053824 14 Dic 18:56 OS XlibSugar.a
-rw-r--r--  1 admin  staff   454957 14 Dic 18:56 OS XlibSugar.fx
drwxr-xr-x  4 admin  staff      136 15 Dic 12:56 iOS
-rw-r--r--  1 admin  staff  3050544 14 Dic 18:57 iOSlibSugar.a
-rw-r--r--  1 admin  staff  1357172 14 Dic 18:57 iOSlibSugar.fx
drwxr-xr-x  4 admin  staff      136 15 Dic 12:56 tvOS

in
/Volumes/MacHDD2/Developmemt/Java/sugar/Sugar.Data/bin

then I use those libs to compile against my project and I confirm that I see as path in the console logs:

01-19 15:42:07.754 17958-17958/? I/System.out: User path /
01-19 15:42:07.754 17958-17958/? I/System.out: App folder path /db
01-19 15:42:07.754 17958-17958/? I/System.out: Database path /db/db.sql
01-19 15:42:07.756 17958-17958/? I/System.out: sql file error
01-19 15:42:07.756 17958-17958/? I/System.out: sugar.SugarIOException: Unable to create folder /db
01-19 15:42:07.756 17958-17958/? I/System.out: Database error

calling

            // file system folder path
		let Separator:Char=Sugar.io.folder.Separator;
		let userLocal:Folder=Sugar.IO.Folder.UserLocal();
		let userLocalPath:String=userLocal.Path;
		let dbPath:String = Sugar.io.Path.Combine(userLocalPath,"db")
		let dbFilePath:String = Sugar.io.Path.Combine(dbPath,"db.sql")
		
		logger.debug("User path \(userLocalPath)");
		logger.debug("App folder path \(dbPath)");
		logger.debug("Database path \(dbFilePath)");

so like it’s building or linking standard Java instead of Android Java.
On iOS the bin are ARM64/ARMv7 as expected.

My project is here: https://github.com/loretoparisi/swift-promise-example

This is what I see from the MANIFEST.MF donno if it helps to understand this issue:

Funny thing! If I take sugar.jar from the Fire (beta) distribution here:

/Applications/Fire.app/Contents/Resources/References/CoreReferences/Cooper/Android

then I get a

java.lang.NoSuchMethodError: No static method setApplicationContext(Ljava/lang/Object;)V in class Lsugar/Environment; or its super classes (declaration of 'sugar.Environment' appears in /data/app/musixmatch.com.sampleandroidapp-2/base.apk)

full stack trace:

01-19 17:19:41.589 20283-20283/? E/AndroidRuntime: FATAL EXCEPTION: main
                                                   Process: musixmatch.com.sampleandroidapp, PID: 20283
                                                   java.lang.NoSuchMethodError: No static method setApplicationContext(Ljava/lang/Object;)V in class Lsugar/Environment; or its super classes (declaration of 'sugar.Environment' appears in /data/app/musixmatch.com.sampleandroidapp-2/base.apk)
                                                       at samplelibrary.SharedClassTest.context(/Volumes/MacHDD2/Developmemt/ParisiLabs/swift-promise-example/StaticLibrary/SharedProject/Library.swift:114)
                                                       at musixmatch.com.sampleandroidapp.MainActivity.onCreate(MainActivity.java:50)
                                                       at android.app.Activity.performCreate(Activity.java:6251)
                                                       at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
                                                       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2369)
                                                       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
                                                       at android.app.ActivityThread.-wrap11(ActivityThread.java)
                                                       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
                                                       at android.os.Handler.dispatchMessage(Handler.java:102)
                                                       at android.os.Looper.loop(Looper.java:148)
                                                       at android.app.ActivityThread.main(ActivityThread.java:5417)
                                                       at java.lang.reflect.Method.invoke(Native Method)
                                                       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
                                                       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
01-19 17:19:41.591 603-793/? W/ActivityManager:   Force finishing activity musixmatch.com.sampleandroidapp/.MainActivity

related to the code

 #if java
	public func context(var context:android.content.Context) ->() {
		Sugar.Environment.ApplicationContext = context;
	}
 #endif

Rebuilding Sugar with the upcoming Fire should properly emit the Android jar now.

Not to mention, the next beta should have the latest Sugar, already.

Great, can’t way to try it! Have a nice weekend!

1 Like

@mh @ck Thanks a lot! I have just updated to the latest Beta 8.3.90.1922 Now it builds against the right Android target, so I can get the right Sugar.data library, therefore the SQLite database is created with success:

01-25 11:47:20.831 5923-5923/? I/System.out: User path /data/user/0/musixmatch.com.sampleandroidapp/files
01-25 11:47:20.831 5923-5923/? I/System.out: App folder path /data/user/0/musixmatch.com.sampleandroidapp/files/db
01-25 11:47:20.831 5923-5923/? I/System.out: Database path /data/user/0/musixmatch.com.sampleandroidapp/files/db/db.sql
01-25 11:47:20.831 5923-5923/? I/System.out: Scanning /data/user/0...
01-25 11:47:20.833 5923-5923/? I/System.out: Database found at /data/user/0/musixmatch.com.sampleandroidapp/files/db/db.sql  

At this point, I’m running the same code as on iOS, but here I get an error when performing the INSERT that is done via the following:

/**
	* Execute insert and return the last insert id
	* @throws SQLiteException
	*/
	func executeInsert(conn:SQLiteConnection!, query:String!, parameters: AnyObject![]) throws -> Int64 {
		let RES:Int64 = conn.ExecuteInsert(query , parameters);
		return RES;
	} //executeInsert

and the calling it against these values:

private func testInsert(conn:SQLiteConnection!) -> Bool {
		do {
				
			let rndIndex=(Sugar.Random()).NextInt();
			let key="USER_"+Sugar.Convert.ToString(rndIndex);
				
			let INSERT = "INSERT OR REPLACE INTO CACHE (cache_key, cache_value, timestamp) VALUES (?,?,?);";
			try executeInsert(conn, query:INSERT , parameters:[key,"PIPPO","20150101"]);
			
		} catch let error as SQLiteException {
			logger.error("sql error",error:error);
			return false;
		}
		return true;
	} //testInsert

The weird error is pretty much like wrong statements parameters error:

Caused by: java.lang.IllegalArgumentException: Cannot bind argument at index 0 because the index is out of range.  The statement has 3 parameters.
                                                     at android.database.sqlite.SQLiteProgram.bind(SQLiteProgram.java:212)
                                                     at android.database.sqlite.SQLiteProgram.bindString(SQLiteProgram.java:166)
                                                     at sugar.data.SQLiteHelpers.BindArgs()
                                                     at samplelibrary.DatabaseStorage.executeInsert__query__parameters

This is calling the base implementation

{$IFDEF ANDROID}
  SQLiteHelpers = public static class
  public
    class method ArgsToString(arr: array of Object): array of String;
    class method BindArgs(st: android.database.sqlite.SQLiteStatement; aArgs: array of Object);
  end;
  {$ENDIF}

Therefore here:

method SQLiteHelpers.BindArgs(st: android.database.sqlite.SQLiteStatement; aArgs: array of Object);
begin
  for i: Integer := 0 to length(aArgs) -1 do begin
    var val := aArgs[i];
    if val = nil then
      st.bindNull(i)
    else if val is Double then
      st.bindDouble(i, Double(val))
    else if val is Single then
      st.bindDouble(i, Single(val))
    else if val is Int64 then
      st.bindLong(i, Int64(val))
    else if val is Int64 then
      st.bindLong(i, Int64(val))
    else if val is array of SByte then 
      st.bindBlob(i, array of SByte(val))
    else 
      st.bindString(i, val.toString);
  end;
end;

According to this, the index should start from 1, I’m not sure if the code does other index offsets, but it seems to start from 0 here (I could be wrong, sorry about that :slightly_smiling:

See: http://stackoverflow.com/questions/25587858/java-lang-illegalargumentexception-cannot-bind-argument-at-index-0-because-the

Note: The same code perfectly runs and works on iOS ARM64 target (database insert with success).

@mh @ck Ok…it worked I just fixed that in Sugar.Data.SQLite.pas_ changing the starting index to 1.

method SQLiteHelpers.BindArgs(st: android.database.sqlite.SQLiteStatement; aArgs: array of Object);
begin
  for i: Integer := 1 to length(aArgs) -1 do begin

and now it works on Android too!!!

Hope to find the fix soon to avoid wrong merges :slight_smile:

hrmm that doesn’t sound right, you’re skipping a parameter now.

Yep:

01-25 17:07:05.298 12322-12322/? I/System.out: Database found at /data/user/0/musixmatch.com.sampleandroidapp/files/db/db.sql
01-25 17:07:05.305 12322-12322/? I/System.out: 2
01-25 17:07:05.305 12322-12322/? I/System.out: PIPPO
01-25 17:07:05.305 12322-12322/? I/System.out: 20150101
01-25 17:07:05.391 12322-12322/? I/System.out: 3
01-25 17:07:05.391 12322-12322/? I/System.out: PIPPO
01-25 17:07:05.391 12322-12322/? I/System.out: 20150101
01-25 17:07:05.394 12322-12335/? D/TEST: Now calling sdk

In fact on iOS I have

31
USER_-189093271
PIPPO

missing a parameter but not crashing. What can be wrong starting from 0?