diff --git a/dependencies.list b/dependencies.list index 4c90a89d30..a3fd56b4ef 100644 --- a/dependencies.list +++ b/dependencies.list @@ -7,10 +7,10 @@ REALM_CORE=10.3.3 MONGODB_REALM_SERVER=2021-03-09 # Common Android settings across projects -GRADLE_BUILD_TOOLS=4.0.0 +GRADLE_BUILD_TOOLS=4.1.0 ANDROID_BUILD_TOOLS=29.0.3 -KOTLIN=1.3.72 -KOTLIN_COROUTINES=1.3.9 +KOTLIN=1.4.31 +KOTLIN_COROUTINES=1.4.2 # Common classpath dependencies gradle=6.5 diff --git a/examples/mongoDbRealmExample/build.gradle b/examples/mongoDbRealmExample/build.gradle index 8bba9f531e..a1f05aadcb 100644 --- a/examples/mongoDbRealmExample/build.gradle +++ b/examples/mongoDbRealmExample/build.gradle @@ -34,13 +34,10 @@ android { buildTypes { // Configure server and App Id. - // The default server is https://realm-dev.mongodb.com/ . Go to that and copy the MongoDB + // The default server is https://realm.mongodb.com/ . Go to that and copy the MongoDB // Realm App Id. - // - // If you are running a local version of MongoDB Realm, modify endpoint accordingly. Most - // likely it is "http://localhost:9090" - def mongodbRealmUrl = "https://realm-dev.mongodb.com" - def appId = "my-app-id" + def mongodbRealmUrl = "https://realm.mongodb.com" + def appId = "counter-app-snuns" debug { buildConfigField "String", "MONGODB_REALM_URL", "\"${mongodbRealmUrl}\"" buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\"" @@ -65,8 +62,9 @@ realm { } dependencies { - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.3.0' + implementation 'com.google.android.material:material:1.3.0' implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/examples/mongoDbRealmExample/src/main/AndroidManifest.xml b/examples/mongoDbRealmExample/src/main/AndroidManifest.xml index eb8829a1c6..51e2ef01c2 100644 --- a/examples/mongoDbRealmExample/src/main/AndroidManifest.xml +++ b/examples/mongoDbRealmExample/src/main/AndroidManifest.xml @@ -18,7 +18,13 @@ + android:label="Login"> + + + diff --git a/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/ClientResetActivity.kt b/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/ClientResetActivity.kt new file mode 100644 index 0000000000..438e9c6821 --- /dev/null +++ b/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/ClientResetActivity.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2021 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.realm.example + +import android.os.Bundle +import android.os.SystemClock +import android.view.View +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import com.mongodb.realm.example.databinding.ActivityClientresetBinding +import io.realm.* +import io.realm.mongodb.sync.* +import me.zhanghai.android.materialprogressbar.MaterialProgressBar +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +/** + * This class is used as an example on how to implement Client Reset. + * + * This activity is launched with `singleInstance` so no matter how many times it is + * started only one instance is running. However, when finished, it will just return to whatever is + * on the top of navigation stack. + * + * Pressing back has been disabled to prevent the [clientResetHelper] from accidentally running + * while another Activity is displayed. + */ +class ClientResetActivity : AppCompatActivity() { + + companion object { + // Track Client Reset errors as a queue of errors as the chance of all Realms + // connected to a single app instance experiencing Client Resets is quite high, e.g. + // in the case where Sync was terminated then restarted on the server. + var RESET_ERRORS = ConcurrentLinkedQueue>() + } + + // Run Client Reset logic on a separate helper thread to make it easier to implement timeouts. + // Note, this Runnable is not particular safe as it will continue running even if the Activity + // is excited + private val clientResetHelper = Runnable { + var errorReported = false; + ClientResetLoop@ while(true) { + val error: Pair = RESET_ERRORS.poll() ?: break + val clientReset: ClientResetRequiredError = error.first + val config: SyncConfiguration = error.second + + // The background Sync Client take about 10 seconds to fully close the connection and thus + // the background Realm. Set timeout to 20 seconds. + var maxWait = 20 + while (Realm.getGlobalInstanceCount(config) > 0) { + if (maxWait == 0) { + runOnUiThread { + progressBar.visibility = View.INVISIBLE + statusView.text = "'${config.realmFileName}' did not fully close, so database could not be reset. Aborting" + } + errorReported = true + break@ClientResetLoop + } else { + maxWait-- + runOnUiThread { + statusView.text = "Waiting for '${config.realmFileName}' to fully close ($maxWait): ${Realm.getGlobalInstanceCount(config)}" + } + SystemClock.sleep(1000) + } + } + clientReset.executeClientReset() + runOnUiThread { + statusView.text = "" + } + } + if (!errorReported) { + finish() + } + } + + private lateinit var binding: ActivityClientresetBinding + + private lateinit var statusView: TextView + private lateinit var progressBar: MaterialProgressBar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_clientreset) + statusView = binding.status + progressBar = binding.progressbar + } + + override fun onResume() { + super.onResume() + Thread(clientResetHelper).start(); + } + + override fun onBackPressed() { + Toast.makeText( + this, + "Pressing 'Back' is disabled while Client Reset is running.", + Toast.LENGTH_LONG + ).show() + } +} diff --git a/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/CounterActivity.kt b/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/CounterActivity.kt index e5279180e3..95300deae4 100644 --- a/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/CounterActivity.kt +++ b/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/CounterActivity.kt @@ -32,10 +32,7 @@ import io.realm.kotlin.syncSession import io.realm.kotlin.where import io.realm.log.RealmLog import io.realm.mongodb.User -import io.realm.mongodb.sync.ProgressListener -import io.realm.mongodb.sync.ProgressMode -import io.realm.mongodb.sync.SyncConfiguration -import io.realm.mongodb.sync.SyncSession +import io.realm.mongodb.sync.* import me.zhanghai.android.materialprogressbar.MaterialProgressBar import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -97,43 +94,37 @@ class CounterActivity : AppCompatActivity() { user = loggedInUser val user = user if (user != null) { - // Create a RealmConfiguration for our user - // Use user id as partition value, so each user gets an unique view. - // FIXME Right now we are using waitForInitialRemoteData and a more advanced - // initialData block due to Sync only supporting ObjectId keys. This should - // be changed once natural keys are supported. + // Create a RealmConfiguration for our user. Use user id as partition value, so each + // user gets an unique view. val config = SyncConfiguration.Builder(user, user.id) .initialData { - if (it.isEmpty) { - it.insert(CRDTCounter()) - } + it.insert(CRDTCounter(user.id)) + } + .clientResetHandler { session, error -> + ClientResetActivity.RESET_ERRORS.add(Pair(error, session.configuration)) + val intent = Intent(this, ClientResetActivity::class.java) + startActivity(intent) } - .waitForInitialRemoteData() .build() - // This will automatically sync all changes in the background for as long as the Realm is open - Realm.getInstanceAsync(config, object: Realm.Callback() { - override fun onSuccess(realm: Realm) { - this@CounterActivity.realm = realm - - counter = realm.where().findFirstAsync() - counter.addChangeListener { obj, _ -> - if (obj.isValid) { - counterView.text = String.format(Locale.US, "%d", counter.count) - } else { - counterView.text = "-" - } + realm = Realm.getInstance(config).also { + counter = it.where().findFirstAsync() + counter.addChangeListener { obj, _ -> + if (obj.isValid) { + counterView.text = String.format(Locale.US, "%d", counter.count) + } else { + counterView.text = "-" } + } - // Setup progress listeners for indeterminate progress bars - session = realm.syncSession - session.run { - addDownloadProgressListener(ProgressMode.INDEFINITELY, downloadListener) - addUploadProgressListener(ProgressMode.INDEFINITELY, uploadListener) - } + // Setup progress listeners for indeterminate progress bars + session = it.syncSession + session.run { + addDownloadProgressListener(ProgressMode.INDEFINITELY, downloadListener) + addUploadProgressListener(ProgressMode.INDEFINITELY, uploadListener) } - }) - counterView.text = "-" + counterView.text = "-" + } } } @@ -144,6 +135,7 @@ class CounterActivity : AppCompatActivity() { removeProgressListener(downloadListener) removeProgressListener(uploadListener) } + // Close Realm here to make sure it is closed when navigating to other Activities. realm?.close() } } @@ -156,15 +148,16 @@ class CounterActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_logout -> { - val user = user - user?.logOutAsync { - if (it.isSuccess) { - realm?.close() - this.user = loggedInUser - } else { - RealmLog.error(it.error.toString()) - } - } +// val user = user +// user?.logOutAsync { +// if (it.isSuccess) { +// realm?.close() +// this.user = loggedInUser +// } else { +// RealmLog.error(it.error.toString()) +// } +// } + ResetHelper.triggerClientReset(user!!.app.sync, realm!!.syncSession) true } else -> super.onOptionsItemSelected(item) diff --git a/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/model/CRDTCounter.kt b/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/model/CRDTCounter.kt index 54d9b97ad1..db967a46f0 100644 --- a/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/model/CRDTCounter.kt +++ b/examples/mongoDbRealmExample/src/main/java/com/mongodb/realm/example/model/CRDTCounter.kt @@ -22,11 +22,15 @@ import io.realm.annotations.RealmField import io.realm.annotations.Required import org.bson.types.ObjectId -open class CRDTCounter : RealmObject() { +open class CRDTCounter(userId: String) : RealmObject() { + + // Required by Realm + constructor(): this("") @PrimaryKey @RealmField("_id") - var id: ObjectId = ObjectId.get() + var id: String = userId + @Required private val counter: MutableRealmInteger = MutableRealmInteger.valueOf(0L) diff --git a/examples/mongoDbRealmExample/src/main/java/io/realm/mongodb/sync/ResetHelper.kt b/examples/mongoDbRealmExample/src/main/java/io/realm/mongodb/sync/ResetHelper.kt new file mode 100644 index 0000000000..2f33c39220 --- /dev/null +++ b/examples/mongoDbRealmExample/src/main/java/io/realm/mongodb/sync/ResetHelper.kt @@ -0,0 +1,9 @@ +package io.realm.mongodb.sync + +class ResetHelper { + companion object { + fun triggerClientReset(sync: Sync, session: SyncSession) { + sync.simulateClientReset(session) + } + } +} \ No newline at end of file diff --git a/examples/mongoDbRealmExample/src/main/res/layout/activity_clientreset.xml b/examples/mongoDbRealmExample/src/main/res/layout/activity_clientreset.xml new file mode 100644 index 0000000000..a45ff28619 --- /dev/null +++ b/examples/mongoDbRealmExample/src/main/res/layout/activity_clientreset.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + +