Americans spent $135.5 billion on back-to-school and back-to-college shopping in 2023, making the season a …
SymmetricDS has supported syncing data to and from Android SQLite databases for some time now. But with the 3.8 series, we now also have support for pushing and pulling files from your Android devices. This could be useful to collect things like signature files captured on the device, log files, or to push files down to your devices as well. Maybe you have a company materials you want to make available offline for instance. Whatever your use case, SymmetricDS now supports bi-directional file sync on the Android platform.
For the remainder of this blog, I want to take you through a step by step (more or less) guide to set up an Android client with SymmetricDS, and to show how to configure and test file sync. If you’re only interested in the file sync portion, skip all the way down to step 10.
You can download the entire project and source code from the example here.
Please make sure to go this this tutorial with SymmetricDS 3.8.4 or greater.
I target a lower SDK for more compatibility and less issues with file system permissions.
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<provider android:name="DbProvider" android:authorities="com.jumpmind.symmetric.DbProvider" android:exported="false"> <grant-uri-permission android:pathPattern=".*" /> </provider>
Add the corresponding DbProvider class.
package com.jumpmind.symds3; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.support.annotation.Nullable; import org.jumpmind.symmetric.android.SQLiteOpenHelperRegistry; import org.jumpmind.symmetric.android.SymmetricService; import org.jumpmind.symmetric.common.ParameterConstants; import java.util.Properties; public class DbProvider extends ContentProvider { private final String REGISTRATION_URL = "http://192.168.1.103:31415/sync/corp-000"; private final String NODE_ID = "android-003"; private final String NODE_GROUP = "store"; final String SQL_CREATE_TABLE_ITEM = "CREATE TABLE IF NOT EXISTS ITEM(\n" + " ITEM_ID INTEGER NOT NULL PRIMARY KEY ,\n" + " NAME VARCHAR\n" + ");"; final String SQL_CREATE_TABLE_ITEM_SELLING_PRICE = "CREATE TABLE IF NOT EXISTS ITEM_SELLING_PRICE(\n" + " ITEM_ID INTEGER NOT NULL,\n" + " STORE_ID VARCHAR NOT NULL,\n" + " PRICE DECIMAL NOT NULL,\n" + " COST DECIMAL,\n" + " PRIMARY KEY (ITEM_ID, STORE_ID)\n" + ");"; final String SQL_CREATE_TABLE_SALE_TRANSACTION = "CREATE TABLE IF NOT EXISTS SALE_TRANSACTION(\n" + " TRAN_ID INTEGER NOT NULL PRIMARY KEY ,\n" + " STORE_ID VARCHAR NOT NULL,\n" + " WORKSTATION VARCHAR NOT NULL,\n" + " DAY VARCHAR NOT NULL,\n" + " SEQ INTEGER NOT NULL\n" + ");\n"; final String SQL_CREATE_TABLE_SALE_RETURN_LINE_ITEM = "CREATE TABLE IF NOT EXISTS SALE_RETURN_LINE_ITEM(\n" + " TRAN_ID INTEGER NOT NULL PRIMARY KEY ,\n" + " ITEM_ID INTEGER NOT NULL,\n" + " PRICE DECIMAL NOT NULL,\n" + " QUANTITY INTEGER NOT NULL,\n" + " RETURNED_QUANTITY INTEGER\n" + ");\n"; public static final String DATABASE_NAME = "symmetric-demo.db"; // Handle to a new DatabaseHelper. private DatabaseHelper mOpenHelper; /** * * This class helps open, create, and upgrade the database file. Set to package visibility * for testing purposes. */ static class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) { // calls the super constructor, requesting the default cursor factory. super(context, DATABASE_NAME, null, 2); } @Override public void onCreate(SQLiteDatabase db) { } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { onCreate(db); } } /** * * Initializes the provider by creating a new DatabaseHelper. onCreate() is called * automatically when Android creates the provider in response to a resolver request from a * client. */ @Override public boolean onCreate() { // Creates a new helper object. Note that the database itself isn't opened until // something tries to access it, and it's only created if it doesn't already exist. mOpenHelper = new DatabaseHelper(getContext()); // Init the DB here mOpenHelper.getWritableDatabase().execSQL(SQL_CREATE_TABLE_ITEM); mOpenHelper.getWritableDatabase().execSQL(SQL_CREATE_TABLE_ITEM_SELLING_PRICE); mOpenHelper.getWritableDatabase().execSQL(SQL_CREATE_TABLE_SALE_TRANSACTION); mOpenHelper.getWritableDatabase().execSQL(SQL_CREATE_TABLE_SALE_RETURN_LINE_ITEM); // Register the database helper, so it can be shared with the SymmetricService SQLiteOpenHelperRegistry.register(DATABASE_NAME, mOpenHelper); Intent intent = new Intent(getContext(), SymmetricService.class); intent.putExtra(SymmetricService.INTENTKEY_SQLITEOPENHELPER_REGISTRY_KEY, DATABASE_NAME); intent.putExtra(SymmetricService.INTENTKEY_REGISTRATION_URL, REGISTRATION_URL); intent.putExtra(SymmetricService.INTENTKEY_EXTERNAL_ID, NODE_ID); intent.putExtra(SymmetricService.INTENTKEY_NODE_GROUP_ID, NODE_GROUP); intent.putExtra(SymmetricService.INTENTKEY_START_IN_BACKGROUND, true); Properties properties = new Properties(); properties.put(ParameterConstants.FILE_SYNC_ENABLE, "true"); properties.put("start.file.sync.tracker.job", "true"); properties.put("start.file.sync.push.job", "true"); properties.put("start.file.sync.pull.job", "true"); properties.put("job.file.sync.pull.period.time.ms", "10000"); intent.putExtra(SymmetricService.INTENTKEY_PROPERTIES, properties); getContext().startService(intent); // Assumes that any failures will be reported by a thrown exception. return true; } @Nullable @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } public String getType(Uri uri) { throw new IllegalArgumentException("Unknown URI " + uri); } @Nullable @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }
Button runButton = (Button)findViewById(R.id.runButton); runButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String sql = sqlTextField.getText().toString(); SQLiteDatabase db = SQLiteOpenHelperRegistry.lookup(DbProvider.DATABASE_NAME).getReadableDatabase(); Cursor cursor = null; try { cursor = db.rawQuery(sql, null); } catch (Exception ex) {}
The INSERT button is wired like this. It simply places an insert statement into the text field for convenience.
Button insertButton = (Button)findViewById(R.id.insertButton); insertButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sqlTextField.setText(INSERT_SQL); } });
First, we need to locate the SymmetricDS sync URL in the android application, and correct it for your environment. Normally you would not hardcode this, but provide it yourself with your application’s configuration mechanism.
Check the “logcat” view to see how your application is doing. In my case, I need to open registration for my new Android node. (http://www.symmetricds.org/doc/3.8/html/user-guide.html#_registration)
[If you happen to see this error at this point: “no such column: value” – make sure you are using SymmetricDS 3.8.4 or greater on the Android client. You might be running into this issue: http://www.symmetricds.org/issues/view.php?id=2776]
When initiating the SymmetricDS service on the Android side, the following properties will enable file File Sync (From DbProvider.java)
Properties properties = new Properties(); properties.put(ParameterConstants.FILE_SYNC_ENABLE, "true"); properties.put("start.file.sync.tracker.job", "true"); properties.put("start.file.sync.push.job", "true"); properties.put("start.file.sync.pull.job", "true"); properties.put("job.file.sync.pull.period.time.ms", "10000"); intent.putExtra(SymmetricService.INTENTKEY_PROPERTIES, properties); getContext().startService(intent);
[My first pass at this resulted in this error:
Destination '/storage/emulated/0/Documents/manuals' directory cannot be created.
This can happen due to an Android permissions problem. There are a few ways to resolve this, but one way is to set the targetSdkVersion less than 23, in your build.gradle file.] Here is the configuration I set on the server for this file sync:
insert into SYM_PARAMETER (EXTERNAL_ID, NODE_GROUP_ID, PARAM_KEY, PARAM_VALUE, CREATE_TIME, LAST_UPDATE_BY, LAST_UPDATE_TIME) values ('ALL','ALL','file.sync.enable','true',{ts '2016-09-13 16:46:29.993'},'no_user',{ts '2016-09-13 16:46:29.993'}); insert into SYM_FILE_TRIGGER (TRIGGER_ID, CHANNEL_ID, RELOAD_CHANNEL_ID, BASE_DIR, RECURSE, INCLUDES_FILES, EXCLUDES_FILES, SYNC_ON_CREATE, SYNC_ON_MODIFIED, SYNC_ON_DELETE, SYNC_ON_CTL_FILE, DELETE_AFTER_SYNC, BEFORE_COPY_SCRIPT, AFTER_COPY_SCRIPT, CREATE_TIME, LAST_UPDATE_BY, LAST_UPDATE_TIME) values ('MANUALS','filesync','filesync_reload','/Users/mmichalek/Documents/manuals',0,null,null,1,1,1,0,0,null,null,{ts '2016-09-13 16:51:51.174'},'no_user',{ts '2016-09-13 16:54:16.305'}); insert into SYM_FILE_TRIGGER_ROUTER (TRIGGER_ID, ROUTER_ID, ENABLED, INITIAL_LOAD_ENABLED, TARGET_BASE_DIR, CONFLICT_STRATEGY, CREATE_TIME, LAST_UPDATE_BY, LAST_UPDATE_TIME) values ('MANUALS','corp_2_store',1,0,'${androidBaseDir}/Documents/manuals','SOURCE_WINS',{ts '2016-09-13 16:52:07.821'},'no_user',{ts '2016-09-13 16:52:07.821'});
You can use the adb console to check if your files have synced down to the device, and to create files for SymmetricDS to push back up. In my example, I was able to do these commands:
$ adb shell $ cd /storage/emulated/0/Documents/manuals $ ls first-sync.pdf second-sync.pdf … etc.