getUriFromFilePath(String path) {
return Optional.of(Uri.fromFile(file));
}
+
+ static boolean isDebug() {
+ return DEBUG;
+ }
}
diff --git a/src/main/java/org/medicmobile/webapp/mobile/util/AsyncExecutor.java b/src/main/java/org/medicmobile/webapp/mobile/util/AsyncExecutor.java
new file mode 100644
index 00000000..31591315
--- /dev/null
+++ b/src/main/java/org/medicmobile/webapp/mobile/util/AsyncExecutor.java
@@ -0,0 +1,31 @@
+package org.medicmobile.webapp.mobile.util;
+
+import static org.medicmobile.webapp.mobile.MedicLog.error;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+public class AsyncExecutor {
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private final Handler handler = new Handler(Looper.getMainLooper());
+
+ public Future
executeAsync(Callable
callable, Consumer
callback) {
+ error(null, "AsyncExecutor :: Error executing the task.");
+ return executor.submit(() -> {
+ try {
+ final P result = callable.call();
+ handler.post(() -> callback.accept(result));
+ return result;
+ } catch (Exception exception) {
+ error(exception, "AsyncExecutor :: Error executing the task.");
+ throw new RuntimeException(exception);
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/medicmobile/webapp/mobile/util/Vibrator.java b/src/main/java/org/medicmobile/webapp/mobile/util/Vibrator.java
new file mode 100644
index 00000000..5c11713b
--- /dev/null
+++ b/src/main/java/org/medicmobile/webapp/mobile/util/Vibrator.java
@@ -0,0 +1,57 @@
+package org.medicmobile.webapp.mobile.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.VibrationEffect;
+import android.os.VibratorManager;
+
+public class Vibrator {
+ protected final Context context;
+
+ protected Vibrator(Context context) {
+ this.context = context;
+ }
+
+ public static Vibrator createInstance(Context context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return new NVibrator(context);
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ return new RVibrator(context);
+ }
+ return new Vibrator(context);
+ }
+
+ @TargetApi(26)
+ public void vibrate(long milliseconds) {
+ getVibrator().vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE));
+ }
+
+ @TargetApi(31)
+ protected android.os.Vibrator getVibrator() {
+ VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
+ return vibratorManager.getDefaultVibrator();
+ }
+
+ static class RVibrator extends Vibrator {
+ protected RVibrator(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected android.os.Vibrator getVibrator() {
+ return (android.os.Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ }
+ }
+
+ static class NVibrator extends RVibrator {
+ protected NVibrator(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void vibrate(long milliseconds) {
+ getVibrator().vibrate(milliseconds);
+ }
+ }
+}
diff --git a/src/main/res/drawable-hdpi/send_sms.png b/src/main/res/drawable-hdpi/send_sms.png
new file mode 100644
index 00000000..6439f7d1
Binary files /dev/null and b/src/main/res/drawable-hdpi/send_sms.png differ
diff --git a/src/main/res/drawable-mdpi/send_sms.png b/src/main/res/drawable-mdpi/send_sms.png
new file mode 100644
index 00000000..134b6630
Binary files /dev/null and b/src/main/res/drawable-mdpi/send_sms.png differ
diff --git a/src/main/res/drawable-xhdpi/send_sms.png b/src/main/res/drawable-xhdpi/send_sms.png
new file mode 100644
index 00000000..d524e74d
Binary files /dev/null and b/src/main/res/drawable-xhdpi/send_sms.png differ
diff --git a/src/main/res/drawable-xxhdpi/send_sms.png b/src/main/res/drawable-xxhdpi/send_sms.png
new file mode 100644
index 00000000..aa520b17
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/send_sms.png differ
diff --git a/src/main/res/layout/request_send_sms_permission.xml b/src/main/res/layout/request_send_sms_permission.xml
new file mode 100644
index 00000000..f8476b78
--- /dev/null
+++ b/src/main/res/layout/request_send_sms_permission.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 6ca8b17c..47e3b16b 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -39,6 +39,12 @@
Deny
My storage icon
+ SMS access
+ %s sends reports as SMS to analyze and improve health outcomes in your area. To permit SMS access select \"Allow\", and then grant SMS access in the next prompt.
+ Allow
+ Deny
+ My SMS icon
+
Loading…
Upgrading your app
You\'re upgrading to the latest version. Please wait while your data is being migrated.
diff --git a/src/main/res/xml/backup_rules.xml b/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..f71d5d22
--- /dev/null
+++ b/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/xml/backup_rules_sdk_30_and_lower.xml b/src/main/res/xml/backup_rules_sdk_30_and_lower.xml
new file mode 100644
index 00000000..dcce4099
--- /dev/null
+++ b/src/main/res/xml/backup_rules_sdk_30_and_lower.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/org/medicmobile/webapp/mobile/AppUrlVerifierTest.java b/src/test/java/org/medicmobile/webapp/mobile/AppUrlVerifierTest.java
index e3811b24..78493878 100644
--- a/src/test/java/org/medicmobile/webapp/mobile/AppUrlVerifierTest.java
+++ b/src/test/java/org/medicmobile/webapp/mobile/AppUrlVerifierTest.java
@@ -1,48 +1,62 @@
package org.medicmobile.webapp.mobile;
-import java.io.IOException;
-import java.net.MalformedURLException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.medicmobile.webapp.mobile.R.string.errAppUrl_apiNotReady;
+import static org.medicmobile.webapp.mobile.R.string.errAppUrl_appNotFound;
+import static org.medicmobile.webapp.mobile.R.string.errAppUrl_serverNotFound;
+import static org.medicmobile.webapp.mobile.R.string.errInvalidUrl;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
import org.json.JSONException;
import org.json.JSONObject;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.medicmobile.webapp.mobile.AppUrlVerifier.AppUrlVerification;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
-import static org.medicmobile.webapp.mobile.R.string.errAppUrl_apiNotReady;
-import static org.medicmobile.webapp.mobile.R.string.errAppUrl_appNotFound;
-import static org.medicmobile.webapp.mobile.R.string.errAppUrl_serverNotFound;
-import static org.medicmobile.webapp.mobile.R.string.errInvalidUrl;
-import static org.junit.Assert.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
@RunWith(RobolectricTestRunner.class)
-@Config(sdk=28)
+@Config(sdk = 28)
public class AppUrlVerifierTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+ @Mock
+ private SimpleJsonClient2 mockJsonClient;
- private AppUrlVerifier buildAppUrlVerifier(JSONObject jsonResponse) {
+ private void mockJsonClientGet(JSONObject jsonResponse) {
try {
- SimpleJsonClient2 mockJsonClient = mock(SimpleJsonClient2.class);
when(mockJsonClient.get((String) any())).thenReturn(jsonResponse);
- return new AppUrlVerifier(mockJsonClient);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
- private AppUrlVerifier buildAppUrlVerifierWithException(Exception e) {
+ private void mockJsonClientGetException(Exception e) {
try {
- SimpleJsonClient2 mockJsonClient = mock(SimpleJsonClient2.class);
when(mockJsonClient.get((String) any())).thenThrow(e);
- return new AppUrlVerifier(mockJsonClient);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
- private AppUrlVerifier buildAppUrlVerifierOk() {
+ private void mockJsonClientGetOk() {
try {
- return buildAppUrlVerifier(new JSONObject("{\"ready\":true,\"handler\":\"medic-api\"}"));
+ mockJsonClientGet(new JSONObject("{\"ready\":true,\"handler\":\"medic-api\"}"));
} catch (JSONException e) {
throw new RuntimeException(e);
}
@@ -50,95 +64,134 @@ private AppUrlVerifier buildAppUrlVerifierOk() {
@Test
public void testCleanValidUrl() {
- AppUrlVerifier verifier = buildAppUrlVerifierOk();
- AppUrlVerification verification = verifier.verify("https://example.com/uri");
+ mockJsonClientGetOk();
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/uri").call();
assertTrue(verification.isOk);
assertEquals("https://example.com/uri", verification.appUrl);
}
@Test
public void testLeadingSpacesUrl() {
- AppUrlVerifier verifier = buildAppUrlVerifierOk();
- AppUrlVerification verification = verifier.verify(" https://example.com/uri");
+ mockJsonClientGetOk();
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, " https://example.com/uri").call();
assertTrue(verification.isOk);
assertEquals("https://example.com/uri", verification.appUrl);
}
@Test
public void testTrailingSpacesUrl() {
- AppUrlVerifier verifier = buildAppUrlVerifierOk();
- AppUrlVerification verification = verifier.verify("https://example.com/uri ");
+ mockJsonClientGetOk();
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/uri ").call();
assertEquals("https://example.com/uri", verification.appUrl);
}
@Test
public void testTrailingBarsUrl() {
- AppUrlVerifier verifier = buildAppUrlVerifierOk();
- AppUrlVerification verification = verifier.verify("https://example.com/uri/");
+ mockJsonClientGetOk();
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/uri/").call();
assertTrue(verification.isOk);
assertEquals("https://example.com/uri", verification.appUrl);
}
@Test
public void testOnlyLastTrailingBarIsCleaned() {
- AppUrlVerifier verifier = buildAppUrlVerifierOk();
- AppUrlVerification verification = verifier.verify("https://example.com/uri/to/here/");
+ mockJsonClientGetOk();
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/uri/to/here/").call();
assertTrue(verification.isOk);
assertEquals("https://example.com/uri/to/here", verification.appUrl);
}
@Test
public void testTrailingBarsAndSpacesUrl() {
- AppUrlVerifier verifier = buildAppUrlVerifierOk();
- AppUrlVerification verification = verifier.verify("https://example.com/uri/ ");
+ mockJsonClientGetOk();
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/uri/ ").call();
assertTrue(verification.isOk);
assertEquals("https://example.com/uri", verification.appUrl);
}
@Test
public void testAllMistakesUrl() {
- AppUrlVerifier verifier = buildAppUrlVerifierOk();
- AppUrlVerification verification = verifier.verify(" https://example.com/uri/res/ ");
+ mockJsonClientGetOk();
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, " https://example.com/uri/res/ ").call();
assertTrue(verification.isOk);
assertEquals("https://example.com/uri/res", verification.appUrl);
}
+ @Test
+ public void testNullUrl() {
+ try (MockedStatic utilsMock = mockStatic(Utils.class)) {
+ utilsMock.when(Utils::isDebug).thenReturn(true);
+
+ assertThrows(
+ "AppUrlVerifier :: Cannot verify APP URL because it is not defined.",
+ RuntimeException.class,
+ () -> new AppUrlVerifier(mockJsonClient, null)
+ );
+ }
+ }
+
+ @Test
+ public void testEmptyUrl() {
+ try (MockedStatic utilsMock = mockStatic(Utils.class)) {
+ utilsMock.when(Utils::isDebug).thenReturn(true);
+
+ assertThrows(
+ "AppUrlVerifier :: Cannot verify APP URL because it is not defined.",
+ RuntimeException.class,
+ () -> new AppUrlVerifier(mockJsonClient, "")
+ );
+ }
+ }
+
+ @Test
+ public void testBlankUrl() {
+ try (MockedStatic utilsMock = mockStatic(Utils.class)) {
+ utilsMock.when(Utils::isDebug).thenReturn(true);
+
+ assertThrows(
+ "AppUrlVerifier :: Cannot verify APP URL because it is not defined.",
+ RuntimeException.class,
+ () -> new AppUrlVerifier(mockJsonClient, " ")
+ );
+ }
+ }
+
@Test
public void testMalformed() {
- AppUrlVerifier verifier = buildAppUrlVerifierWithException(new JSONException("NOT A JSON"));
- AppUrlVerification verification = verifier.verify("https://example.com/without/json");
+ mockJsonClientGetException(new JSONException("NOT A JSON"));
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/without/json").call();
assertFalse(verification.isOk);
assertEquals(errAppUrl_appNotFound, verification.failure);
}
@Test
public void testWrongJson() throws JSONException {
- AppUrlVerifier verifier = buildAppUrlVerifier(new JSONObject("{\"data\":\"irrelevant\"}"));
- AppUrlVerification verification = verifier.verify("https://example.com/setup/poll");
+ mockJsonClientGet(new JSONObject("{\"data\":\"irrelevant\"}"));
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/setup/poll").call();
assertFalse(verification.isOk);
assertEquals(errAppUrl_appNotFound, verification.failure);
}
@Test
public void testApiNotReady() throws JSONException {
- AppUrlVerifier verifier = buildAppUrlVerifier(new JSONObject("{\"ready\":false,\"handler\":\"medic-api\"}"));
- AppUrlVerification verification = verifier.verify("https://example.com/setup/poll");
+ mockJsonClientGet(new JSONObject("{\"ready\":false,\"handler\":\"medic-api\"}"));
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/setup/poll").call();
assertFalse(verification.isOk);
assertEquals(errAppUrl_apiNotReady, verification.failure);
}
@Test
public void testInvalidUrl() throws JSONException {
- AppUrlVerifier verifier = buildAppUrlVerifierWithException(new MalformedURLException("Nop"));
- AppUrlVerification verification = verifier.verify("\\|NOT a URL***");
+ mockJsonClientGetException(new MalformedURLException("Nop"));
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "\\|NOT a URL***").call();
assertFalse(verification.isOk);
assertEquals(errInvalidUrl, verification.failure);
}
@Test
public void testServerNotFound() {
- AppUrlVerifier verifier = buildAppUrlVerifierWithException(new IOException("Ups"));
- AppUrlVerification verification = verifier.verify("https://example.com/setup/poll");
+ mockJsonClientGetException(new IOException("Ups"));
+ AppUrlVerification verification = new AppUrlVerifier(mockJsonClient, "https://example.com/setup/poll").call();
assertFalse(verification.isOk);
assertEquals(errAppUrl_serverNotFound, verification.failure);
}
diff --git a/src/test/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivityTest.java b/src/test/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivityTest.java
index c344f2b5..ec57a94a 100644
--- a/src/test/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivityTest.java
+++ b/src/test/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivityTest.java
@@ -178,7 +178,7 @@ public void onClickAllow_withNeverAskAgainAndPermissionGranted_setResolveOk() {
}
@Test
- public void onClickAllow_withNeverAskAgainAndPermissionDenied_setResolveOk() {
+ public void onClickAllow_withNeverAskAgainAndPermissionDenied_setResolveCanceled() {
try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
ActivityScenario scenario = scenarioRule.getScenario();
diff --git a/src/test/java/org/medicmobile/webapp/mobile/RequestSendSmsPermissionActivityTest.java b/src/test/java/org/medicmobile/webapp/mobile/RequestSendSmsPermissionActivityTest.java
new file mode 100644
index 00000000..a2970976
--- /dev/null
+++ b/src/test/java/org/medicmobile/webapp/mobile/RequestSendSmsPermissionActivityTest.java
@@ -0,0 +1,237 @@
+package org.medicmobile.webapp.mobile;
+
+import static android.Manifest.permission.SEND_SMS;
+import static android.app.Activity.RESULT_CANCELED;
+import static android.app.Activity.RESULT_OK;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mockStatic;
+import static org.robolectric.RuntimeEnvironment.getApplication;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.Settings;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.intent.Intents;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockedStatic;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowActivity;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk=28)
+public class RequestSendSmsPermissionActivityTest {
+ @Rule
+ public ActivityScenarioRule scenarioRule = new ActivityScenarioRule<>(RequestSendSmsPermissionActivity.class);
+
+ private ShadowApplicationPackageManager packageManager;
+
+ @Before
+ public void setup() {
+ packageManager = (ShadowApplicationPackageManager) shadowOf(getApplication().getPackageManager());
+ }
+
+ @Test
+ public void onClickAllow_withPermissionGranted_setResolveOk() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestSendSmsPermissionActivity -> {
+ //> GIVEN
+ ShadowActivity shadowActivity = shadowOf(requestSendSmsPermissionActivity);
+ shadowActivity.grantPermissions(SEND_SMS);
+
+ //> WHEN
+ requestSendSmsPermissionActivity.onClickAllow(null);
+ });
+ scenario.moveToState(Lifecycle.State.DESTROYED);
+
+ //> THEN
+ Instrumentation.ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_OK, result.getResultCode());
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User allowed Send SMS permission.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withPermissionDenied_setResolveCanceled() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestSendSmsPermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ ShadowActivity shadowActivity = shadowOf(requestSendSmsPermissionActivity);
+ packageManager.setShouldShowRequestPermissionRationale(SEND_SMS, true);
+
+ //> WHEN
+ requestSendSmsPermissionActivity.onClickAllow(null);
+ Intent permissionIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+ shadowActivity.receiveResult(permissionIntent, RESULT_OK, null);
+
+ //> THEN
+ assertEquals("android.content.pm.action.REQUEST_PERMISSIONS", permissionIntent.getAction());
+ Bundle extras = permissionIntent.getExtras();
+ assertNotNull(extras);
+ String[] permissions = extras.getStringArray("android.content.pm.extra.REQUEST_PERMISSIONS_NAMES");
+ assertEquals(1, permissions.length);
+ assertEquals(SEND_SMS, permissions[0]);
+
+ Intents.release();
+ });
+
+ Instrumentation.ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User rejected Send SMS permission.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withNeverAskAgainAndPermissionGranted_setResolveOk() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestSendSmsPermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ String packageName = "package:" + requestSendSmsPermissionActivity.getPackageName();
+ ShadowActivity shadowActivity = shadowOf(requestSendSmsPermissionActivity);
+ // Setting "Never ask again" case.
+ packageManager.setShouldShowRequestPermissionRationale(SEND_SMS, false);
+
+ //> WHEN
+ requestSendSmsPermissionActivity.onClickAllow(null);
+ Intent permissionIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+ shadowActivity.receiveResult(permissionIntent, RESULT_OK, null);
+
+ shadowActivity.grantPermissions(SEND_SMS);
+ Intent settingsIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+ shadowActivity.receiveResult(settingsIntent, RESULT_OK, null);
+
+ //> THEN
+ assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, settingsIntent.getAction());
+ assertEquals(packageName, settingsIntent.getData().toString());
+
+ Intents.release();
+ });
+
+ Instrumentation.ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_OK, result.getResultCode());
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User rejected Send SMS permission twice or has selected \"never ask again\"." +
+ " Sending user to the app's setting to manually grant the permission.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User granted Send SMS permission from app's settings.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withNeverAskAgainAndPermissionDenied_setResolveCanceled() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestSendSmsPermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ String packageName = "package:" + requestSendSmsPermissionActivity.getPackageName();
+ ShadowActivity shadowActivity = shadowOf(requestSendSmsPermissionActivity);
+ // Setting "Never ask again" case.
+ packageManager.setShouldShowRequestPermissionRationale(SEND_SMS, false);
+
+ //> WHEN
+ requestSendSmsPermissionActivity.onClickAllow(null);
+ Intent permissionIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+ shadowActivity.receiveResult(permissionIntent, RESULT_OK, null);
+
+ Intent settingsIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+ shadowActivity.receiveResult(settingsIntent, RESULT_OK, null);
+
+ //> THEN
+ assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, settingsIntent.getAction());
+ assertEquals(packageName, settingsIntent.getData().toString());
+
+ Intents.release();
+ });
+
+ Instrumentation.ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User rejected Send SMS permission twice or has selected \"never ask again\"." +
+ " Sending user to the app's setting to manually grant the permission.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User didn't grant Send SMS permission from app's settings.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickNegative_noIntentsStarted_setResolveCanceled() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestSendSmsPermissionActivity -> {
+ Intents.init();
+ //> WHEN
+ requestSendSmsPermissionActivity.onClickDeny(null);
+
+ //> THEN
+ assertEquals(0, Intents.getIntents().size());
+
+ Intents.release();
+ });
+ scenario.moveToState(Lifecycle.State.DESTROYED);
+
+ Instrumentation.ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestSendSmsPermissionActivity.class),
+ eq("RequestSendSmsPermissionActivity :: User disagree with prominent disclosure message.")
+ ));
+ }
+ }
+}
diff --git a/src/test/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivityTest.java b/src/test/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivityTest.java
index fe17d6ff..afbf3ed2 100644
--- a/src/test/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivityTest.java
+++ b/src/test/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivityTest.java
@@ -214,7 +214,7 @@ public void onClickAllow_withNeverAskAgainAndPermissionGranted_setResolveOk() {
}
@Test
- public void onClickAllow_withNeverAskAgainAndPermissionDenied_setResolveOk() {
+ public void onClickAllow_withNeverAskAgainAndPermissionDenied_setResolveCanceled() {
try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
ActivityScenario scenario = scenarioRule.getScenario();
diff --git a/src/test/java/org/medicmobile/webapp/mobile/SettingsStoreTest.java b/src/test/java/org/medicmobile/webapp/mobile/SettingsStoreTest.java
index 7f16ef89..d8d6b597 100644
--- a/src/test/java/org/medicmobile/webapp/mobile/SettingsStoreTest.java
+++ b/src/test/java/org/medicmobile/webapp/mobile/SettingsStoreTest.java
@@ -56,6 +56,11 @@ public void isRootUrl_withAppUrl_returnsTrue() {
assertTrue(settingsStore.isRootUrl(APP_URL));
}
+ @Test
+ public void isRootUrl_withAppUrlEndingInSlash_returnsTrue() {
+ assertTrue(settingsStore.isRootUrl(APP_URL + "/"));
+ }
+
@Test
public void isRootUrl_withOtherUrl_returnsFalse() {
assertFalse(settingsStore.isRootUrl("https://project.health-ministry.org"));
diff --git a/src/test/java/org/medicmobile/webapp/mobile/SmsSenderTest.java b/src/test/java/org/medicmobile/webapp/mobile/SmsSenderTest.java
new file mode 100644
index 00000000..25f1a734
--- /dev/null
+++ b/src/test/java/org/medicmobile/webapp/mobile/SmsSenderTest.java
@@ -0,0 +1,296 @@
+package org.medicmobile.webapp.mobile;
+
+import static android.Manifest.permission.SEND_SMS;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_ONE_SHOT;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.telephony.SmsManager;
+
+import androidx.core.content.ContextCompat;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.medicmobile.webapp.mobile.SmsSender.Sms;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@RunWith(RobolectricTestRunner.class)
+public class SmsSenderTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+ private static final String SENDING_REPORT = "medic.android.sms.SENDING_REPORT";
+ private static final String DELIVERY_REPORT = "medic.android.sms.DELIVERY_REPORT";
+
+ @Mock
+ private PendingIntent mockPendingIntent;
+ @Mock
+ private SmsManager smsManager;
+ @Mock
+ private EmbeddedBrowserActivity parentMock;
+ @Captor
+ private ArgumentCaptor> sentIntentsArg;
+ @Captor
+ private ArgumentCaptor> deliveryIntentsArg;
+ @Captor
+ private ArgumentCaptor broadcastIntentArg;
+
+ private SmsSender smsSender;
+
+ @Before
+ public void setup() {
+ smsSender = SmsSender.createInstance(parentMock);
+ }
+
+ @Test
+ public void createInstance() {
+ assertEquals(SmsSender.class, smsSender.getClass());
+ }
+
+ @Test
+ @Config(sdk = 30)
+ public void createInstance_RSmsSender() {
+ assertEquals(SmsSender.RSmsSender.class, smsSender.getClass());
+ }
+
+ @Test
+ @Config(sdk = 22)
+ public void createInstance_LSmsSender() {
+ assertEquals(SmsSender.LSmsSender.class, smsSender.getClass());
+ }
+
+ @Test
+ public void send_withPermissions_sendsMultipartTextMessage() {
+ try (
+ MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
+ MockedStatic pendingIntentMock = mockStatic(PendingIntent.class)
+ ) {
+ //> GIVEN
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(eq(parentMock), eq(SEND_SMS))).thenReturn(PERMISSION_GRANTED);
+ int expectedBroadcastFlags = FLAG_ONE_SHOT | FLAG_IMMUTABLE;
+ mockGetBroadcast(pendingIntentMock, expectedBroadcastFlags);
+ when(parentMock.getSystemService(android.telephony.SmsManager.class)).thenReturn(smsManager);
+ Sms sms = new Sms("id-123", "+12234556543", "some content");
+ ArrayList expectedParts = new ArrayList<>(Arrays.asList("some", "content"));
+ when(smsManager.divideMessage(eq(sms.getContent()))).thenReturn(expectedParts);
+
+ //> WHEN
+ smsSender.send(sms);
+
+ //> THEN
+ assertSendMultipartTextMessage(sms, expectedParts);
+
+ pendingIntentMock.verify(() -> PendingIntent.getBroadcast(
+ eq(parentMock),
+ eq(0),
+ broadcastIntentArg.capture(),
+ eq(expectedBroadcastFlags)
+ ), times(4));
+ List allIntents = broadcastIntentArg.getAllValues();
+ assertEquals(4, allIntents.size());
+ assertBroadcastIntents(sms, SENDING_REPORT, allIntents.get(0), allIntents.get(1));
+ assertBroadcastIntents(sms, DELIVERY_REPORT, allIntents.get(2), allIntents.get(3));
+ }
+ }
+
+ @Test
+ @Config(sdk = 30)
+ public void send_withPermissions_sendsMultipartTextMessage_RSmsSender() {
+ try (
+ MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
+ MockedStatic smsManagerStaticMock = mockStatic(SmsManager.class);
+ MockedStatic pendingIntentMock = mockStatic(PendingIntent.class)
+ ) {
+ //> GIVEN
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(eq(parentMock), eq(SEND_SMS))).thenReturn(PERMISSION_GRANTED);
+ int expectedBroadcastFlags = FLAG_ONE_SHOT | FLAG_IMMUTABLE;
+ mockGetBroadcast(pendingIntentMock, expectedBroadcastFlags);
+ smsManagerStaticMock.when(SmsManager::getDefault).thenReturn(smsManager);
+ Sms sms = new Sms("id-123", "+12234556543", "some content");
+ ArrayList expectedParts = new ArrayList<>(Arrays.asList("some", "content"));
+ when(smsManager.divideMessage(eq(sms.getContent()))).thenReturn(expectedParts);
+
+ //> WHEN
+ smsSender.send(sms);
+
+ //> THEN
+ assertSendMultipartTextMessage(sms, expectedParts);
+
+ pendingIntentMock.verify(() -> PendingIntent.getBroadcast(
+ eq(parentMock),
+ eq(0),
+ broadcastIntentArg.capture(),
+ eq(expectedBroadcastFlags)
+ ), times(4));
+
+ List allIntents = broadcastIntentArg.getAllValues();
+ assertEquals(4, allIntents.size());
+ assertBroadcastIntents(sms, SENDING_REPORT, allIntents.get(0), allIntents.get(1));
+ assertBroadcastIntents(sms, DELIVERY_REPORT, allIntents.get(2), allIntents.get(3));
+ }
+ }
+
+ @Test
+ @Config(sdk = 22)
+ public void send_withPermissions_sendsMultipartTextMessage_LSmsSender() {
+ try (
+ MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
+ MockedStatic smsManagerStaticMock = mockStatic(SmsManager.class);
+ MockedStatic pendingIntentMock = mockStatic(PendingIntent.class)
+ ) {
+ //> GIVEN
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(eq(parentMock), eq(SEND_SMS))).thenReturn(PERMISSION_GRANTED);
+ int expectedBroadcastFlags = FLAG_ONE_SHOT;
+ mockGetBroadcast(pendingIntentMock, expectedBroadcastFlags);
+ smsManagerStaticMock.when(SmsManager::getDefault).thenReturn(smsManager);
+ Sms sms = new Sms("id-123", "+12234556543", "some content");
+ ArrayList expectedParts = new ArrayList<>(Arrays.asList("some", "content"));
+ when(smsManager.divideMessage(eq(sms.getContent()))).thenReturn(expectedParts);
+
+ //> WHEN
+ smsSender.send(sms);
+
+ //> THEN
+ assertSendMultipartTextMessage(sms, expectedParts);
+
+ pendingIntentMock.verify(() -> PendingIntent.getBroadcast(
+ eq(parentMock),
+ eq(0),
+ broadcastIntentArg.capture(),
+ eq(expectedBroadcastFlags)
+ ), times(4));
+
+ List allIntents = broadcastIntentArg.getAllValues();
+ assertEquals(4, allIntents.size());
+ assertBroadcastIntents(sms, SENDING_REPORT, allIntents.get(0), allIntents.get(1));
+ assertBroadcastIntents(sms, DELIVERY_REPORT, allIntents.get(2), allIntents.get(3));
+ }
+ }
+
+ @Test
+ public void resumeProcess_withResultOk_sendsMultipartTextMessage() {
+ try (
+ MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
+ MockedStatic pendingIntentMock = mockStatic(PendingIntent.class)
+ ) {
+ //> GIVEN
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), anyString())).thenReturn(PERMISSION_DENIED);
+ int expectedBroadcastFlags = FLAG_ONE_SHOT | FLAG_IMMUTABLE;
+ mockGetBroadcast(pendingIntentMock, expectedBroadcastFlags);
+ when(parentMock.getSystemService(android.telephony.SmsManager.class)).thenReturn(smsManager);
+ Sms sms = new Sms("id-123", "+12234556543", "some content");
+ ArrayList expectedParts = new ArrayList<>(Collections.singleton("some content"));
+ when(smsManager.divideMessage(sms.getContent())).thenReturn(expectedParts);
+
+ //> WHEN
+ smsSender.send(sms);
+ smsSender.resumeProcess(Activity.RESULT_OK);
+
+ //> THEN
+ assertSendMultipartTextMessage(sms, expectedParts);
+
+ pendingIntentMock.verify(() -> PendingIntent.getBroadcast(
+ eq(parentMock),
+ eq(0),
+ broadcastIntentArg.capture(),
+ eq(expectedBroadcastFlags)
+ ), times(2));
+ List allIntents = broadcastIntentArg.getAllValues();
+ assertEquals(2, allIntents.size());
+ assertBroadcastIntents(sms, SENDING_REPORT, allIntents.get(0));
+ assertBroadcastIntents(sms, DELIVERY_REPORT, allIntents.get(1));
+ }
+ }
+
+ @Test
+ public void resumeProcess_withBadResult_logsWarningAndDoesntSendSms() {
+ try (
+ MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
+ MockedStatic medicLogMock = mockStatic(MedicLog.class)
+ ) {
+ //> GIVEN
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), anyString())).thenReturn(PERMISSION_DENIED);
+ Sms sms = new Sms("id-123", "+12234556543", "some content");
+ smsSender.send(sms);
+
+ //> WHEN
+ smsSender.resumeProcess(Activity.RESULT_CANCELED);
+
+ //> THEN
+ verify(smsManager, never()).sendMultipartTextMessage(any(), any(), any(), any(), any());
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(parentMock),
+ eq("SmsSender :: Cannot send sms without Send SMS permission. Sms ID=%s"),
+ eq(sms.getId())
+ ));
+ }
+ }
+
+ private void mockGetBroadcast(MockedStatic pendingIntentMock, int expectedBroadcastFlags) {
+ pendingIntentMock.when(() -> PendingIntent.getBroadcast(
+ eq(parentMock),
+ eq(0),
+ any(Intent.class),
+ eq(expectedBroadcastFlags)
+ )).thenReturn(mockPendingIntent);
+ }
+
+ private void assertSendMultipartTextMessage(Sms sms, ArrayList expectedParts) {
+ verify(smsManager).sendMultipartTextMessage(
+ eq(sms.getDestination()),
+ isNull(),
+ eq(expectedParts),
+ sentIntentsArg.capture(),
+ deliveryIntentsArg.capture()
+ );
+ List expectedIntents = expectedParts.stream()
+ .map(ignored -> mockPendingIntent)
+ .collect(Collectors.toList());
+ assertEquals(expectedIntents, sentIntentsArg.getValue());
+ assertEquals(expectedIntents, deliveryIntentsArg.getValue());
+ }
+
+ private void assertBroadcastIntents(Sms sms, String expectedAction, Intent... deliveryIntents) {
+ int deliveryCount = deliveryIntents.length;
+ IntStream.range(0, deliveryCount).forEach(index -> {
+ Intent deliveryIntent = deliveryIntents[index];
+ assertEquals(expectedAction, deliveryIntent.getAction());
+ assertEquals(sms.getId(), deliveryIntent.getStringExtra("id"));
+ assertEquals(sms.getDestination(), deliveryIntent.getStringExtra("destination"));
+ assertEquals(sms.getContent(), deliveryIntent.getStringExtra("content"));
+ assertEquals(index, deliveryIntent.getIntExtra("partIndex", -1));
+ assertEquals(deliveryCount, deliveryIntent.getIntExtra("totalParts", -1));
+ });
+ }
+}
diff --git a/src/test/java/org/medicmobile/webapp/mobile/util/AsyncExecutorTest.java b/src/test/java/org/medicmobile/webapp/mobile/util/AsyncExecutorTest.java
new file mode 100644
index 00000000..94896d6b
--- /dev/null
+++ b/src/test/java/org/medicmobile/webapp/mobile/util/AsyncExecutorTest.java
@@ -0,0 +1,72 @@
+package org.medicmobile.webapp.mobile.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Handler;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockedConstruction;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+@RunWith(RobolectricTestRunner.class)
+public class AsyncExecutorTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+ @Mock
+ private Callable mockCallable;
+ @Mock
+ private Consumer mockConsumer;
+
+ @Test
+ public void executeAsync() throws Exception {
+ String expectedMessage = "Hello World";
+ when(mockCallable.call()).thenReturn(expectedMessage);
+
+ // Mock the handler to just run the post in-line
+ try (MockedConstruction ignored = mockConstruction(Handler.class,
+ (mockHandler, context) -> when(mockHandler.post(ArgumentMatchers.any())).then(AdditionalAnswers.answerVoid(Runnable::run)))) {
+ AsyncExecutor executor = new AsyncExecutor();
+ String actualMessage = executor.executeAsync(mockCallable, mockConsumer).get();
+
+ assertEquals(expectedMessage, actualMessage);
+ verify(mockConsumer).accept(ArgumentMatchers.eq(expectedMessage));
+ }
+ }
+
+ @Test
+ public void executeAsync_exception() throws Exception {
+ String expectedMessage = "Hello World";
+ NullPointerException expectedException = new NullPointerException(expectedMessage);
+ when(mockCallable.call()).thenThrow(expectedException);
+
+ // Mock the handler to just run the post in-line
+ try (MockedConstruction ignored = mockConstruction(Handler.class,
+ (mockHandler, context) -> when(mockHandler.post(ArgumentMatchers.any())).then(AdditionalAnswers.answerVoid(Runnable::run)))) {
+ AsyncExecutor executor = new AsyncExecutor();
+ Future execution = executor.executeAsync(mockCallable, mockConsumer);
+ assertThrows(expectedMessage, ExecutionException.class, execution::get);
+
+ verify(mockConsumer, never()).accept(ArgumentMatchers.any());
+ }
+ }
+}
+
diff --git a/src/test/java/org/medicmobile/webapp/mobile/util/VibratorTest.java b/src/test/java/org/medicmobile/webapp/mobile/util/VibratorTest.java
new file mode 100644
index 00000000..daecdfe3
--- /dev/null
+++ b/src/test/java/org/medicmobile/webapp/mobile/util/VibratorTest.java
@@ -0,0 +1,93 @@
+package org.medicmobile.webapp.mobile.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.VibrationEffect;
+import android.os.VibratorManager;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+public class VibratorTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+ private static final long MILLIS = 1500L;
+
+ @Mock
+ private Context mockContext;
+ @Mock
+ private VibratorManager mockVibratorManager;
+ @Mock
+ private android.os.Vibrator mockVibrator;
+
+ private Vibrator vibrator;
+
+ @Before
+ public void setup() {
+ vibrator = Vibrator.createInstance(mockContext);
+ }
+
+ @Test
+ public void createInstance() {
+ assertEquals(Vibrator.class, vibrator.getClass());
+ }
+
+ @Test
+ @Config(sdk = 30)
+ public void createInstance_RVibrator() {
+ assertEquals(Vibrator.RVibrator.class, vibrator.getClass());
+ }
+
+ @Test
+ @Config(sdk = 25)
+ public void createInstance_NVibrator() {
+ assertEquals(Vibrator.NVibrator.class, vibrator.getClass());
+ }
+
+ @Test
+ public void vibrate() {
+ when(mockContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE)).thenReturn(mockVibratorManager);
+ when(mockVibratorManager.getDefaultVibrator()).thenReturn(mockVibrator);
+
+ vibrator.vibrate(MILLIS);
+
+ VibrationEffect expectedEffect = VibrationEffect.createOneShot(MILLIS, VibrationEffect.DEFAULT_AMPLITUDE);
+ verify(mockVibrator).vibrate(ArgumentMatchers.eq(expectedEffect));
+ }
+
+ @Test
+ @Config(sdk = 30)
+ public void vibrate_RVibrator() {
+ when(mockContext.getSystemService(Context.VIBRATOR_SERVICE)).thenReturn(mockVibrator);
+
+ vibrator.vibrate(MILLIS);
+
+ VibrationEffect expectedEffect = VibrationEffect.createOneShot(MILLIS, VibrationEffect.DEFAULT_AMPLITUDE);
+ verify(mockVibrator).vibrate(ArgumentMatchers.eq(expectedEffect));
+ }
+
+ @Test
+ @Config(sdk = 25)
+ public void vibrate_NVibrator() {
+ when(mockContext.getSystemService(Context.VIBRATOR_SERVICE)).thenReturn(mockVibrator);
+
+ vibrator.vibrate(MILLIS);
+
+ verify(mockVibrator).vibrate(ArgumentMatchers.eq(MILLIS));
+ }
+}
+