diff --git a/android/core/src/main/java/com/mongodb/stitch/android/core/internal/net/AndroidNetworkMonitor.java b/android/core/src/main/java/com/mongodb/stitch/android/core/internal/net/AndroidNetworkMonitor.java index eaa3128f8..99fe39e26 100644 --- a/android/core/src/main/java/com/mongodb/stitch/android/core/internal/net/AndroidNetworkMonitor.java +++ b/android/core/src/main/java/com/mongodb/stitch/android/core/internal/net/AndroidNetworkMonitor.java @@ -21,6 +21,8 @@ import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.os.AsyncTask; + import com.mongodb.stitch.core.internal.net.NetworkMonitor; import java.util.HashSet; @@ -56,9 +58,39 @@ public synchronized void removeNetworkStateListener(@Nonnull final StateListener @Override public void onReceive(final Context context, final Intent intent) { - final Set listenersCopy = new HashSet<>(listeners); - for (final StateListener listener : listenersCopy) { - listener.onNetworkStateChanged(); + // Dispatch our network change callback to a background thread, + // since our listeners may block the main thread. + // See https://developer.android.com/guide/components/broadcasts#effects-process-state + final PendingResult pendingResult = goAsync(); + final NetworkStateChangedTask asyncTask = + new NetworkStateChangedTask(pendingResult, new HashSet<>(listeners)); + asyncTask.execute(); + } + + private static final class NetworkStateChangedTask extends AsyncTask { + private final Set listeners; + private final PendingResult pendingResult; + + private NetworkStateChangedTask( + final PendingResult result, + final Set listeners + ) { + this.pendingResult = result; + this.listeners = listeners; + } + + @Override + protected Void doInBackground(final Void... v) { + for (final StateListener listener: listeners) { + listener.onNetworkStateChanged(); + } + return null; + } + + @Override + protected void onPostExecute(final Void v) { + super.onPostExecute(v); + pendingResult.finish(); } } } diff --git a/android/examples/stress-tests/.gitignore b/android/examples/stress-tests/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/android/examples/stress-tests/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/examples/stress-tests/build.gradle b/android/examples/stress-tests/build.gradle new file mode 100644 index 000000000..d7cc0da0c --- /dev/null +++ b/android/examples/stress-tests/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.application' +apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'jacoco-android' + +buildscript { + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'digital.wup:android-maven-publish:3.3.0' + classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4' + } +} + +android { + compileSdkVersion target_api + defaultConfig { + minSdkVersion min_api + targetSdkVersion target_api + } +} + +dependencies { + implementation project(':android:stitch-android-sdk') + implementation project(':android:android-services:stitch-android-services-mongodb-remote') + implementation "com.android.support:support-v4:${support_library_version}" + implementation "com.android.support:appcompat-v7:${support_library_version}" + implementation "com.android.support:recyclerview-v7:${support_library_version}" + implementation "com.android.support.constraint:constraint-layout:1.1.0" +} diff --git a/android/examples/stress-tests/proguard-rules.pro b/android/examples/stress-tests/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/android/examples/stress-tests/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/examples/stress-tests/src/main/AndroidManifest.xml b/android/examples/stress-tests/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8c006d7cd --- /dev/null +++ b/android/examples/stress-tests/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/examples/stress-tests/src/main/java/com/mongodb/stitch/android/examples/stresstests/MainActivity.java b/android/examples/stress-tests/src/main/java/com/mongodb/stitch/android/examples/stresstests/MainActivity.java new file mode 100644 index 000000000..cbbcae4c2 --- /dev/null +++ b/android/examples/stress-tests/src/main/java/com/mongodb/stitch/android/examples/stresstests/MainActivity.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018-present MongoDB, 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.stitch.android.examples.stresstests; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.mongodb.stitch.android.core.Stitch; +import com.mongodb.stitch.android.core.StitchAppClient; +import com.mongodb.stitch.android.core.auth.StitchUser; +import com.mongodb.stitch.android.services.mongodb.remote.RemoteMongoClient; +import com.mongodb.stitch.android.services.mongodb.remote.RemoteMongoCollection; +import com.mongodb.stitch.android.services.mongodb.remote.Sync; +import com.mongodb.stitch.core.auth.providers.anonymous.AnonymousCredential; +import com.mongodb.stitch.core.services.mongodb.remote.sync.DefaultSyncConflictResolvers; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.Random; + +import org.bson.Document; +import org.bson.types.ObjectId; + +public class MainActivity extends AppCompatActivity { + + private Button addSingleDocButton; + private Button addManyDocsButton; + private Button clearDocsButton; + + private EditText etManyDocs; + private TextView label; + + private StitchAppClient stitchAppClient; + private RemoteMongoCollection coll; + private Sync syncedColl; + + private static final String TAG = MainActivity.class.getName(); + + private static final Random RANDOM = new Random(); + + private void initializeSync() { + stitchAppClient.getAuth().loginWithCredential(new AnonymousCredential()).addOnSuccessListener( + stitchUser -> { + coll = stitchAppClient + .getServiceClient(RemoteMongoClient.factory, "mongodb-atlas") + .getDatabase("stress") + .getCollection("tests"); + + syncedColl = coll.sync(); + syncedColl.configure( + DefaultSyncConflictResolvers.remoteWins(), + (documentId, event) -> { + Log.i(TAG, String.format("Got event for doc %s: %s", documentId, event)); + updateLabels(); + }, + (documentId, error) -> Log.e( + TAG, String.format("Got sync error for doc %s: %s", documentId, error) + ) + ); + updateLabels(); + }); + + } + + private void updateLabels() { + syncedColl.getSyncedIds().addOnSuccessListener(syncedIds -> label.setText(String.format( + Locale.US, + "# of synced docs: %d", + syncedIds.size() + ))); + } + + private void syncNewDocument() { + final StitchUser user = stitchAppClient.getAuth().getUser(); + if (user == null) { + return; + } + final Document docToInsert = new Document() + .append("_id", new ObjectId()) + .append("owner_id", stitchAppClient.getAuth().getUser().getId()) + .append("message", String.format(Locale.US, "%d", RANDOM.nextInt())); + + syncedColl.insertOne(docToInsert).addOnCompleteListener(task -> updateLabels()); + } + + private void syncManyDocuments(final int numberOfDocs) { + final StitchUser user = stitchAppClient.getAuth().getUser(); + if (user == null) { + return; + } + + final ArrayList docsToInsert = new ArrayList<>(); + + for (int i = 0; i < numberOfDocs; ++i) { + final Document docToInsert = new Document() + .append("_id", new ObjectId()) + .append("owner_id", stitchAppClient.getAuth().getUser().getId()) + .append("message", String.format(Locale.US, "%d", RANDOM.nextInt())); + + docsToInsert.add(docToInsert); + } + + syncedColl.insertMany(docsToInsert).addOnCompleteListener(task -> updateLabels()); + } + + private void clearAllDocuments() { + final StitchUser user = stitchAppClient.getAuth().getUser(); + if (user == null) { + return; + } + + syncedColl.deleteMany(new Document()).addOnCompleteListener(task -> updateLabels()); + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (!Stitch.hasAppClient("stitch-tests-js-sdk-jntlj")) { + Stitch.initializeDefaultAppClient("stitch-tests-js-sdk-jntlj"); + } + + stitchAppClient = Stitch.getDefaultAppClient(); + initializeSync(); + + addSingleDocButton = findViewById(R.id.button); + addManyDocsButton = findViewById(R.id.button3); + clearDocsButton = findViewById(R.id.button2); + etManyDocs = findViewById(R.id.numInput); + + label = findViewById(R.id.textView); + + addSingleDocButton.setOnClickListener(v -> syncNewDocument()); + + addManyDocsButton.setOnClickListener(v -> { + final int numberOfDocsToInsert = Integer.decode(etManyDocs.getText().toString()); + syncManyDocuments(numberOfDocsToInsert); + }); + + clearDocsButton.setOnClickListener(v -> clearAllDocuments()); + } +} diff --git a/android/examples/stress-tests/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/examples/stress-tests/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..1f6bb2906 --- /dev/null +++ b/android/examples/stress-tests/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/examples/stress-tests/src/main/res/drawable/ic_launcher_background.xml b/android/examples/stress-tests/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..0d025f9bf --- /dev/null +++ b/android/examples/stress-tests/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/examples/stress-tests/src/main/res/layout/activity_main.xml b/android/examples/stress-tests/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..fbfd91762 --- /dev/null +++ b/android/examples/stress-tests/src/main/res/layout/activity_main.xml @@ -0,0 +1,61 @@ + + + + + +