'Porting an iOS Objective-c app to Android Java [closed]

My app is a medical data viewer, where patients wear a sensor that transmits data by Bluetooth low energy. The app was developed in Objective C, targeting the iOS platform. Now the app needs to be ported to the Android platform.

The current design and implementation for iOS is as follows:

  • communication - Objective C, specific to the Core Bluetooth API
  • data/persistence - Objective C, using FMDatabase as the interface to SQLite
  • algorithms/logic - Objective C
  • ui - JavaScript/HTML5 based on Phonegap

Since the communication is specific to the Core Bluetooth API, it will have to be re-written for Android. The ui layer should be readily portable without much change as it fully depndendt on Phonegap. Yet for the persistence and logic layers I am looking for a way to either convert them automatically to Android, or re-write them in such a way that they are reusable for both platforms.

What is the best software engineering approach to implement a cross-platform app like this?



Solution 1:[1]

Google has some open source projects that do this.

You will need to use SVN to access these repositories. Here are the links:

Java to Objective C: http://code.google.com/p/j2objc/

Objective C to Java : http://code.google.com/p/objc2j/

Good luck!

Solution 2:[2]

Your best bet is to use Apportable. It's a platform that provides a port of clang, the objective-c runtime, and most of the frameworks on iOS (including UIKit).

There isn't a Core Bluetooth wrapper yet but you can call the java APIs from their platform for that. FMDatabase will work fine and the Phone gap interface should in theory work fine.

I would avoid the code generators suggestions though. They will end up eating a lot of time reimplement everything you already built if you have a type of significate code base.

Solution 3:[3]

I've used O2J - Objective-C to Java Converter for a similar scenario and it worked very well.

It will do a great job on your algorithms/logic without much work.

It's customizable so you can add you own translations for your Bluetooth code. You may be able to get by translating the bluetooth method calls directly to java if the APIs work the same but they probably don't. It's best to have a layer of indirection in your Objective-C code for the bluetooth to make it really easy to supply a Android specific implementation. For example create a BluetoothHelper.m and a BluetoothHelper.java and the translation will go much smoother.

I have used it for projects which used FMDatabase. For the FMDatabase part we already have FMDatabase/FMResultSet as the layer of indirection! I implemented FMDatabase/FMResultSet myself since the API for sqlite Objective-c (c based sqlite functions) is too different from Android. O2J helped me get started on translating FMDatabase/FMResultSet and this is what I ended up with...

FMDatabase:

public class FMDatabase
{

    private SQLiteDatabase database;
    private HashMap<String, SQLiteStatement> compiled;

    public FMDatabase(SQLiteDatabase database)
    {
        this.database = database;
    }

    public FMResultSet executeQuery_arguments(String sql, Object... args)
    {

        synchronized (database)
        {
            String[] selectionArgs = objectArgsAsStrings(args);
            Cursor rawQuery = database.rawQuery(sql, selectionArgs);
            return new FMResultSet(rawQuery);
        }
    }

    public FMResultSet executeQuery(String sql, Object... args)
    {
        synchronized (database)
        {
            String[] selectionArgs = objectArgsAsStrings(args);
            Cursor rawQuery = database.rawQuery(sql, selectionArgs);
            return new FMResultSet(rawQuery);
        }
    }

    public String debugQuery(String sql, Object...args)
    {
        StringBuilder sb = new StringBuilder();
        FMResultSet rs = executeQuery(sql, args);
        rs.setupColumnNames();
        HashMap names = rs.columnNameToIndexMap();
        Set ks = names.keySet();
        for (Object k : ks)
        {
            sb.append(k);
            sb.append("\t");
        }
        sb.append("\n");
        while(rs.next())
        {
            for (Object k : ks)
            {
                String key = k.toString();
                if(rs.getType(key) == Cursor.FIELD_TYPE_STRING)
                {
                    sb.append(rs.stringForColumn(key));
                }
                else if(rs.getType(key) == Cursor.FIELD_TYPE_INTEGER)
                {
                    sb.append(rs.longForColumn(key));
                }
                else if(rs.getType(key) == Cursor.FIELD_TYPE_FLOAT)
                {
                    sb.append(rs.doubleForColumn(key));
                }
                else if(rs.getType(key) == Cursor.FIELD_TYPE_BLOB)
                {
                    sb.append(rs.stringForColumn(key));
                }
                else
                {
                    sb.append("<NOT STRING>");
                }
                sb.append("\t");
            }
            sb.append("\n");
        }
        return sb.toString();
    }

    public String[] objectArgsAsStrings(Object... args)
    {
        String[] selectionArgs = new String[args.length];
        for (int i = 0; i < args.length; i++)
        {
            Object o = args[i];
            if(o instanceof Date)
            {
                selectionArgs[i] = Long.toString(((Date) o).getTime());
            }
            else if(o instanceof Boolean)
            {
                selectionArgs[i] = ((Boolean) o).booleanValue() ? "TRUE" : "FALSE";
            }
            else
            {
                selectionArgs[i] = args[i] == null ? "" : o.toString();
            }
        }
        return selectionArgs;
    }

    public boolean executeUpdate_arguments(String sql, Object... args)
    {
        synchronized (database)
        {
            String[] selectionArgs = objectArgsAsStrings(args);
            database.execSQL(sql, selectionArgs);
            return true;
        }
    }

    public boolean executeUpdate(String sql, Object... args)
    {
        synchronized (database)
        {
            SQLiteStatement statement = bindToCachedCompiledStatement(sql, args);
            statement.execute();
            return true;
        }
    }

    private SQLiteStatement bindToCachedCompiledStatement(String sql, Object... args)
    {
        HashMap<String, SQLiteStatement> statments = getCompiledStatements();
        SQLiteStatement statement = statments.get(sql);
        if (statement == null)
        {
            statement = database.compileStatement(sql);
            statments.put(sql, statement);
        }
        statement.clearBindings();
//      bindAllArgsAsStrings(statement, objectArgsAsStrings(args));
        bindAllArgs(statement, args);
        return statement;
    }

    private void bindAllArgs(SQLiteStatement statement, Object[] bindArgs)
    {
        if (bindArgs == null)
        {
            return;
        }
        int size = bindArgs.length;
        for (int i = 0; i < size; i++)
        {
            Object arg = bindArgs[i];
            int index = i + 1;
            if(arg == null)
            {
                statement.bindNull(index);
            }
            else if (arg instanceof String)
            {
                statement.bindString(index, (String) arg);
            }
            else if (arg instanceof Double || arg instanceof Float)
            {
                Number numArg = (Number) arg;
                statement.bindDouble(index, numArg.doubleValue());
            }
            else if (arg instanceof Integer || arg instanceof Long)
            {
                Number numArg = (Number) arg;
                statement.bindDouble(index, numArg.longValue());
            }
            else
            {
                statement.bindString(index, arg.toString());
            }
        }
    }

    public long executeInsert(String string, Object... args)
    {
        synchronized (database)
        {
            SQLiteStatement statement = bindToCachedCompiledStatement(string, args);
            try
            {
                return statement.executeInsert();
            }
            catch (Exception e)
            {
                Log.i("STD", "No Rows inserted", e);
                return 0;
            }
        }
    }

    public void bindAllArgsAsStrings(SQLiteStatement statement, String[] bindArgs)
    {
        if (bindArgs == null)
        {
            return;
        }
        int size = bindArgs.length;
        for (int i = 0; i < size; i++)
        {
            statement.bindString(i + 1, bindArgs[i]);
        }
    }

    private HashMap<String, SQLiteStatement> getCompiledStatements()
    {
        if (compiled == null)
        {
            compiled = new HashMap<String, SQLiteStatement>();
        }
        return compiled;
    }

    public boolean rollback()
    {
        synchronized (database)
        {
            database.execSQL("ROLLBACK;");
        }
        return true;
    }

    public boolean commit()
    {
        synchronized (database)
        {
            database.execSQL("COMMIT;");
        }
        return true;
    }

    public boolean beginDeferredTransaction()
    {
        synchronized (database)
        {
            database.execSQL("BEGIN DEFERRED TRANSACTION;");
        }
        return true;
    }

    public boolean beginTransaction()
    {
        synchronized (database)
        {
            database.execSQL("BEGIN EXCLUSIVE TRANSACTION;");
        }
        return true;
    }

    public boolean open()
    {
        return true;
    }

    public void setShouldCacheStatements(boolean shouldCacheStatements)
    {
        // TODO
    }

}

FMResultSet:

public class FMResultSet
{
    private boolean columnNamesSetup;
    private HashMap<String, Number> columnNameToIndexMap;
    private Cursor rawQuery;

    public FMResultSet(Cursor rawQuery)
    {
        this.rawQuery = rawQuery;
    }

    public void close()
    {
        rawQuery.close();
    }

    public void setupColumnNames()
    {

        if (columnNameToIndexMap == null)
        {
            this.setColumnNameToIndexMap(new HashMap());
        }

        int columnCount = rawQuery.getColumnCount();

        int columnIdx = 0;
        for (columnIdx = 0; columnIdx < columnCount; columnIdx++)
        {
            columnNameToIndexMap.put(rawQuery.getColumnName(columnIdx).toLowerCase(), new Integer(columnIdx));
        }
        columnNamesSetup = true;
    }

    public boolean next()
    {
        return rawQuery.moveToNext();
    }

    public int columnIndexForName(String columnName)
    {

        if (!columnNamesSetup)
        {
            this.setupColumnNames();
        }

        columnName = columnName.toLowerCase();

        Number n = columnNameToIndexMap.get(columnName);

        if (n != null)
        {
            return NumberValueUtil.intVal(n);
        }

        Log.i("StdLog", String.format("Warning: I could not find the column named '%s'.", columnName));

        return -1;
    }

    public int intForColumn(String columnName)
    {

        if (!columnNamesSetup)
        {
            this.setupColumnNames();
        }

        int columnIdx = this.columnIndexForName(columnName);

        if (columnIdx == -1)
        {
            return 0;
        }

        return intForColumnIndex(columnIdx);
    }

    public int intForColumnIndex(int columnIdx)
    {
        return rawQuery.getInt(columnIdx);
    }

    public long longForColumn(String columnName)
    {

        if (!columnNamesSetup)
        {
            this.setupColumnNames();
        }

        int columnIdx = this.columnIndexForName(columnName);

        if (columnIdx == -1)
        {
            return 0;
        }

        return longForColumnIndex(columnIdx);
    }

    public long longForColumnIndex(int columnIdx)
    {
        return (long) rawQuery.getLong(columnIdx);
    }

    public boolean boolForColumn(String columnName)
    {
        return (this.intForColumn(columnName) != 0);
    }

    public boolean boolForColumnIndex(int columnIdx)
    {
        return (this.intForColumnIndex(columnIdx) != 0);
    }

    public double doubleForColumn(String columnName)
    {

        if (!columnNamesSetup)
        {
            this.setupColumnNames();
        }

        int columnIdx = this.columnIndexForName(columnName);

        if (columnIdx == -1)
        {
            return 0;
        }

        return doubleForColumnIndex(columnIdx);
    }

    public double doubleForColumnIndex(int columnIdx)
    {
        return rawQuery.getDouble(columnIdx);
    }

    public String stringForColumnIndex(int columnIdx)
    {
        return rawQuery.getString(columnIdx);
    }

    public String stringForColumn(String columnName)
    {

        if (!columnNamesSetup)
        {
            this.setupColumnNames();
        }

        int columnIdx = this.columnIndexForName(columnName);

        if (columnIdx == -1)
        {
            return null;
        }

        return this.stringForColumnIndex(columnIdx);
    }

    public Date dateForColumn(String columnName)
    {

        if (!columnNamesSetup)
        {
            this.setupColumnNames();
        }

        int columnIdx = this.columnIndexForName(columnName);

        if (columnIdx == -1)
        {
            return null;
        }

        return new Date((this.longForColumn(columnName)));
    }

    public Date dateForColumnIndex(int columnIdx)
    {
        return new Date((this.longForColumnIndex(columnIdx)));
    }

    public byte[] dataForColumn(String columnName)
    {
        if (!columnNamesSetup)
        {
            this.setupColumnNames();
        }

        int columnIdx = this.columnIndexForName(columnName);

        if (columnIdx == -1)
        {
            return null;
        }

        return this.dataForColumnIndex(columnIdx);
    }

    public byte[] dataForColumnIndex(int columnIdx)
    {

        return rawQuery.getBlob(columnIdx);
    }

    public HashMap columnNameToIndexMap()
    {
        return columnNameToIndexMap;
    }

    public void setColumnNameToIndexMap(HashMap value)
    {

        columnNameToIndexMap = value;
    }

    @SuppressLint("NewApi")
    public int getType(String string)
    {
        return rawQuery.getType(columnIndexForName(string));
    }

}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Anup Cowkur
Solution 2
Solution 3 Cal