diff --git a/android-java/app/build.gradle.kts b/android-java/app/build.gradle.kts index 125486953..475e78a9e 100644 --- a/android-java/app/build.gradle.kts +++ b/android-java/app/build.gradle.kts @@ -6,7 +6,6 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) } fun loadEnvProperties(): Properties { @@ -38,6 +37,10 @@ fun loadEnvProperties(): Properties { // // More information can be found here: // https://docs.ditto.live/sdk/latest/install-guides/java/android#integrating-and-initializing +fun envValue(prop: Properties, key: String): String { + return prop[key]?.toString()?.trim('"') ?: "" +} + androidComponents { onVariants { val prop = loadEnvProperties() @@ -45,7 +48,7 @@ androidComponents { "DITTO_APP_ID", BuildConfigField( "String", - "\"${prop["DITTO_APP_ID"]}\"", + "\"${envValue(prop, "DITTO_APP_ID")}\"", "Ditto application ID" ) ) @@ -53,7 +56,7 @@ androidComponents { "DITTO_PLAYGROUND_TOKEN", BuildConfigField( "String", - "\"${prop["DITTO_PLAYGROUND_TOKEN"]}\"", + "\"${envValue(prop, "DITTO_PLAYGROUND_TOKEN")}\"", "Ditto online playground authentication token" ) ) @@ -62,7 +65,7 @@ androidComponents { "DITTO_AUTH_URL", BuildConfigField( "String", - "\"${prop["DITTO_AUTH_URL"]}\"", + "\"${envValue(prop, "DITTO_AUTH_URL")}\"", "Ditto Auth URL" ) ) @@ -71,7 +74,7 @@ androidComponents { "DITTO_WEBSOCKET_URL", BuildConfigField( "String", - "\"${prop["DITTO_WEBSOCKET_URL"]}\"", + "\"${envValue(prop, "DITTO_WEBSOCKET_URL")}\"", "Ditto Websocket URL" ) ) @@ -114,7 +117,6 @@ android { } buildFeatures { buildConfig = true - compose = true } // This ensures Ditto can produce meaningful stack traces packaging { @@ -129,12 +131,6 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) implementation(libs.ditto) implementation(libs.androidx.recyclerview) implementation(libs.material) @@ -143,8 +139,4 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation("androidx.test.espresso:espresso-contrib:3.6.1") - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) } diff --git a/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt b/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt new file mode 100644 index 000000000..bf3a6e3c2 --- /dev/null +++ b/android-java/app/src/main/java/com/example/dittotasks/DittoHelper.kt @@ -0,0 +1,75 @@ +package com.example.dittotasks + +import com.ditto.kotlin.Ditto +import com.ditto.kotlin.DittoAuthenticationProvider +import com.ditto.kotlin.DittoConfig +import com.ditto.kotlin.DittoException +import com.ditto.kotlin.DittoFactory +import com.ditto.kotlin.DittoQueryResult +import com.ditto.kotlin.DittoStoreObserver +import com.ditto.kotlin.DittoSyncSubscription +import kotlinx.coroutines.runBlocking +import java.util.function.Consumer + +/** + * Bridges Ditto v5 Kotlin SDK suspend functions for Java callers. + */ +object DittoHelper { + + @JvmStatic + fun createDitto(appId: String, serverUrl: String): Ditto { + val config = DittoConfig( + databaseId = appId, + connect = DittoConfig.Connect.Server(serverUrl) + ) + return DittoFactory.create(config) + } + + @JvmStatic + fun setupAuth(ditto: Ditto, token: String) { + ditto.auth?.let { auth -> + auth.expirationHandler = { dittoInstance, _ -> + dittoInstance.auth?.login(token, DittoAuthenticationProvider.development()) + } + } + } + + @JvmStatic + @Throws(DittoException::class) + fun execute(ditto: Ditto, query: String, args: Map) { + runBlocking { + ditto.store.execute(query, args) + } + } + + @JvmStatic + fun registerSubscription(ditto: Ditto, query: String): DittoSyncSubscription { + return ditto.sync.registerSubscription(query) + } + + @JvmStatic + fun registerObserver( + ditto: Ditto, + query: String, + callback: Consumer + ): DittoStoreObserver { + return ditto.store.registerObserver(query) { result -> + callback.accept(result) + } + } + + @JvmStatic + fun startSync(ditto: Ditto) { + ditto.sync.start() + } + + @JvmStatic + fun stopSync(ditto: Ditto) { + ditto.sync.stop() + } + + @JvmStatic + fun isSyncActive(ditto: Ditto): Boolean { + return ditto.sync.isActive + } +} diff --git a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java index f06dce793..0ff7d95d2 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java +++ b/android-java/app/src/main/java/com/example/dittotasks/MainActivity.java @@ -1,7 +1,10 @@ package com.example.dittotasks; +import android.Manifest; import android.app.AlertDialog; +import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -15,25 +18,18 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.ditto.kotlin.Ditto; +import com.ditto.kotlin.DittoStoreObserver; +import com.ditto.kotlin.DittoSyncSubscription; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import kotlin.Unit; -import live.ditto.Ditto; -import live.ditto.DittoDependencies; -import live.ditto.DittoError; -import live.ditto.DittoIdentity; -import live.ditto.DittoStoreObserver; -import live.ditto.DittoSyncSubscription; -import live.ditto.android.DefaultAndroidDittoDependencies; -import live.ditto.transports.DittoSyncPermissions; -import live.ditto.transports.DittoTransportConfig; - public class MainActivity extends ComponentActivity { private TaskAdapter taskAdapter; private SwitchCompat syncSwitch; @@ -47,9 +43,6 @@ public class MainActivity extends ComponentActivity { private String DITTO_AUTH_URL = BuildConfig.DITTO_AUTH_URL; private String DITTO_WEBSOCKET_URL = BuildConfig.DITTO_WEBSOCKET_URL; - // This is required to be set to false to use the correct URLs - private Boolean DITTO_ENABLE_CLOUD_SYNC = true; - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -104,16 +97,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { void initDitto() { Log.d("DittoInit", "=== Starting Ditto initialization ==="); - - // Enable Ditto's internal debug logging (if available) - Log.d("DittoInit", "Ditto Logger class not available in this version, using Android Log instead"); - + Log.d("DittoInit", "DITTO_APP_ID: " + DITTO_APP_ID); Log.d("DittoInit", "DITTO_PLAYGROUND_TOKEN: " + (DITTO_PLAYGROUND_TOKEN != null ? "Present" : "NULL")); Log.d("DittoInit", "DITTO_AUTH_URL: " + DITTO_AUTH_URL); - Log.d("DittoInit", "DITTO_WEBSOCKET_URL: " + DITTO_WEBSOCKET_URL); - Log.d("DittoInit", "DITTO_ENABLE_CLOUD_SYNC: " + DITTO_ENABLE_CLOUD_SYNC); - + // Skip permission requests during testing to avoid permission dialogs if (!isInstrumentationTest()) { Log.d("DittoInit", "Requesting permissions..."); @@ -124,78 +112,43 @@ void initDitto() { Log.d("DittoInit", "Starting Ditto SDK initialization..."); try { - Log.d("DittoInit", "Creating AndroidDependencies..."); - DittoDependencies androidDependencies = new DefaultAndroidDittoDependencies(getApplicationContext()); - Log.d("DittoInit", "AndroidDependencies created successfully"); - - /* - * Setup Ditto Identity - * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing - */ - Log.d("DittoInit", "Creating DittoIdentity.OnlinePlayground..."); - var identity = new DittoIdentity - .OnlinePlayground( - androidDependencies, - DITTO_APP_ID, - DITTO_PLAYGROUND_TOKEN, - DITTO_ENABLE_CLOUD_SYNC, // This is required to be set to false to use the correct URLs - DITTO_AUTH_URL); - Log.d("DittoInit", "DittoIdentity created successfully"); - + // Create Ditto with server connection + // https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing Log.d("DittoInit", "Creating Ditto instance..."); - ditto = new Ditto(androidDependencies, identity); + ditto = DittoHelper.createDitto(DITTO_APP_ID, DITTO_AUTH_URL); Log.d("DittoInit", "Ditto instance created successfully"); - //https://docs.ditto.live/sdk/latest/sync/customizing-transport-configurations - Log.d("DittoInit", "Updating transport config..."); - ditto.updateTransportConfig(config -> { - config.getConnect().getWebsocketUrls().add(DITTO_WEBSOCKET_URL); - - // lambda must return Kotlin Unit which corresponds to 'void' in Java - return kotlin.Unit.INSTANCE; - }); - Log.d("DittoInit", "Transport config updated"); - - // disable sync with v3 peers, required for DQL - Log.d("DittoInit", "Disabling sync with v3..."); - ditto.disableSyncWithV3(); - Log.d("DittoInit", "Sync with v3 disabled"); - - // Disable DQL strict mode - // when set to false, collection definitions are no longer required. SELECT queries will return and display all fields by default. - // https://docs.ditto.live/dql/strict-mode - Log.d("DittoInit", "Setting DQL strict mode to false..."); - ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false"); - Log.d("DittoInit", "DQL strict mode disabled"); + // Set up authentication handler (must be set before sync.start()) + Log.d("DittoInit", "Setting up authentication..."); + DittoHelper.setupAuth(ditto, DITTO_PLAYGROUND_TOKEN); + Log.d("DittoInit", "Authentication configured"); // register subscription // https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions Log.d("DittoInit", "Registering subscription..."); - taskSubscription = ditto.sync.registerSubscription("SELECT * FROM tasks"); + taskSubscription = DittoHelper.registerSubscription(ditto, "SELECT * FROM tasks"); Log.d("DittoInit", "Subscription registered"); // register observer for live query // https://docs.ditto.live/sdk/latest/crud/observing-data-changes#setting-up-store-observers Log.d("DittoInit", "Registering observer..."); - taskObserver = ditto.store.registerObserver("SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC", null, result -> { - Log.d("DittoInit", "Observer callback triggered with " + result.getItems().size() + " items"); - var tasks = result.getItems().stream().map(Task::fromQueryItem).collect(Collectors.toCollection(ArrayList::new)); - runOnUiThread(() -> { - Log.d("DittoInit", "Updating UI with " + tasks.size() + " tasks"); - taskAdapter.setTasks(new ArrayList<>(tasks)); - }); - return Unit.INSTANCE; - }); + taskObserver = DittoHelper.registerObserver(ditto, + "SELECT * FROM tasks WHERE deleted=false ORDER BY title ASC", + result -> { + Log.d("DittoInit", "Observer callback triggered with " + result.getItems().size() + " items"); + var tasks = result.getItems().stream().map(Task::fromQueryItem).collect(Collectors.toCollection(ArrayList::new)); + runOnUiThread(() -> { + Log.d("DittoInit", "Updating UI with " + tasks.size() + " tasks"); + taskAdapter.setTasks(new ArrayList<>(tasks)); + }); + }); Log.d("DittoInit", "Observer registered"); Log.d("DittoInit", "Starting Ditto sync..."); - ditto.startSync(); + DittoHelper.startSync(ditto); Log.d("DittoInit", "=== Ditto initialization completed successfully ==="); - } catch (DittoError e) { - Log.e("DittoInit", "DittoError during initialization: " + e.getMessage(), e); - e.printStackTrace(); } catch (Exception e) { - Log.e("DittoInit", "Unexpected error during Ditto initialization: " + e.getMessage(), e); + Log.e("DittoInit", "Error during Ditto initialization: " + e.getMessage(), e); e.printStackTrace(); } } @@ -213,8 +166,27 @@ private boolean isInstrumentationTest() { // Request permissions for Ditto // https://docs.ditto.live/sdk/latest/install-guides/java#requesting-permissions-at-runtime void requestPermissions() { - DittoSyncPermissions permissions = new DittoSyncPermissions(this); - String[] missing = permissions.missingPermissions(permissions.requiredPermissions()); + List permissions = new ArrayList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_ADVERTISE); + permissions.add(Manifest.permission.BLUETOOTH_CONNECT); + permissions.add(Manifest.permission.BLUETOOTH_SCAN); + } + if (Build.VERSION.SDK_INT <= 32) { + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION); + } + if (Build.VERSION.SDK_INT <= 30) { + permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissions.add(Manifest.permission.NEARBY_WIFI_DEVICES); + } + + String[] missing = permissions.stream() + .filter(p -> checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) + .toArray(String[]::new); + if (missing.length > 0) { this.requestPermissions(missing, 0); } @@ -226,28 +198,25 @@ private void createTask(String title) { task.put("done", false); task.put("deleted", false); - HashMap args = new HashMap<>(); - args.put("task", task); - try { + Map args = Map.of("task", task); + try { // Add tasks into the ditto collection using DQL INSERT statement // https://docs.ditto.live/sdk/latest/crud/write#inserting-documents - ditto.store.execute("INSERT INTO tasks DOCUMENTS (:task)", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "INSERT INTO tasks DOCUMENTS (:task)", args); + } catch (Exception e) { e.printStackTrace(); } } private void editTaskTitle(Task task, String newTitle) { - HashMap args = new HashMap<>(); - args.put("id", task.getId()); - args.put("title", newTitle); + Map args = Map.of("id", task.getId(), "title", newTitle); try { // Update tasks into the ditto collection using DQL UPDATE statement // https://docs.ditto.live/sdk/latest/crud/update#updating - ditto.store.execute("UPDATE tasks SET title=:title WHERE _id=:id", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "UPDATE tasks SET title=:title WHERE _id=:id", args); + } catch (Exception e) { e.printStackTrace(); } } @@ -257,16 +226,14 @@ private void toggleTask(Task task) { Log.i("MainActivity", "Ditto disabled - toggle task ignored: " + task.getTitle()); return; } - - HashMap args = new HashMap<>(); - args.put("id", task.getId()); - args.put("done", !task.isDone()); + + Map args = Map.of("id", task.getId(), "done", !task.isDone()); try { // Update tasks into the ditto collection using DQL UPDATE statement // https://docs.ditto.live/sdk/latest/crud/update#updating - ditto.store.execute("UPDATE tasks SET done=:done WHERE _id=:id", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "UPDATE tasks SET done=:done WHERE _id=:id", args); + } catch (Exception e) { e.printStackTrace(); } } @@ -276,14 +243,14 @@ private void deleteTask(Task task) { Log.i("MainActivity", "Ditto disabled - delete task ignored: " + task.getTitle()); return; } - - HashMap args = new HashMap<>(); - args.put("id", task.getId()); + + Map args = Map.of("id", task.getId()); + try { // UPDATE DQL Statement using Soft-Delete pattern // https://docs.ditto.live/sdk/latest/crud/delete#soft-delete-pattern - ditto.store.execute("UPDATE tasks SET deleted=true WHERE _id=:id", args); - } catch (DittoError e) { + DittoHelper.execute(ditto, "UPDATE tasks SET deleted=true WHERE _id=:id", args); + } catch (Exception e) { e.printStackTrace(); } } @@ -293,7 +260,7 @@ private void toggleSync() { return; } - boolean isSyncActive = ditto.isSyncActive(); + boolean isSyncActive = DittoHelper.isSyncActive(ditto); var nextColor = isSyncActive ? null : ColorStateList.valueOf(0xFFBB86FC); var nextText = isSyncActive ? "Sync Inactive" : "Sync Active"; @@ -301,14 +268,14 @@ private void toggleSync() { // https://docs.ditto.live/sdk/latest/sync/start-and-stop-sync try { if (isSyncActive) { - ditto.stopSync(); + DittoHelper.stopSync(ditto); } else { - ditto.startSync(); + DittoHelper.startSync(ditto); } syncSwitch.setChecked(!isSyncActive); syncSwitch.setTrackTintList(nextColor); syncSwitch.setText(nextText); - } catch (DittoError e) { + } catch (Exception e) { e.printStackTrace(); } } diff --git a/android-java/app/src/main/java/com/example/dittotasks/Task.java b/android-java/app/src/main/java/com/example/dittotasks/Task.java index f9f71d105..f06109e49 100644 --- a/android-java/app/src/main/java/com/example/dittotasks/Task.java +++ b/android-java/app/src/main/java/com/example/dittotasks/Task.java @@ -2,7 +2,10 @@ import java.util.Optional; -import live.ditto.DittoQueryResultItem; +import com.ditto.kotlin.DittoQueryResultItem; + +import org.json.JSONException; +import org.json.JSONObject; public class Task { private Optional id; @@ -22,12 +25,16 @@ public Task(String id, String title, boolean done, boolean deleted) { } public static Task fromQueryItem(DittoQueryResultItem item) { - var map = item.getValue(); - return new Task( - (String) map.get("_id"), - (String) map.get("title"), - Boolean.TRUE.equals(map.get("done")), - Boolean.TRUE.equals(map.get("deleted"))); + try { + JSONObject json = new JSONObject(item.jsonString()); + return new Task( + json.optString("_id", null), + json.optString("title", null), + json.optBoolean("done", false), + json.optBoolean("deleted", false)); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse task from query result", e); + } } public String getId() { diff --git a/android-java/gradle/libs.versions.toml b/android-java/gradle/libs.versions.toml index 14bc28de6..5c38551d9 100644 --- a/android-java/gradle/libs.versions.toml +++ b/android-java/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -ditto = "4.13.1" +ditto = "5.0.0-rc.3" agp = "8.7.3" constraintlayout = "2.2.0" kotlin = "2.0.0" @@ -17,7 +17,7 @@ recyclerviewV7 = "28.0.0" [libraries] androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -ditto = { module = "live.ditto:ditto", version.ref = "ditto" } +ditto = { group = "com.ditto", name = "ditto-kotlin-android", version.ref = "ditto" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }