diff --git a/build.gradle b/build.gradle
index e80c10e2..3bbf2fcc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -387,6 +387,8 @@ dependencies {
// Latest version of androidx.core requires Android 12+
// noinspection GradleDependency
implementation 'androidx.core:core:1.6.0'
+ implementation 'androidx.activity:activity:1.3.1'
+ implementation 'androidx.fragment:fragment:1.3.6'
compileOnly 'com.github.spotbugs:spotbugs-annotations:4.5.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
testImplementation 'junit:junit:4.13.2'
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 2cf89a2a..e9495bf1 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -47,7 +47,9 @@
android:screenOrientation="portrait"/>
-
+
IGNORE_RESULT = new ValueCallback() {
public void onReceiveValue(String result) { /* ignore */ }
};
@@ -194,7 +191,8 @@ protected void onStop() {
backButtonHandler);
}
- @Override protected void onActivityResult(int requestCd, int resultCode, Intent intent) {
+ @Override
+ protected void onActivityResult(int requestCd, int resultCode, Intent intent) {
Optional requestCodeOpt = RequestCode.valueOf(requestCd);
if (!requestCodeOpt.isPresent()) {
@@ -212,14 +210,17 @@ protected void onStop() {
this.filePickerHandler.processResult(resultCode, intent);
return;
case GRAB_MRDT_PHOTO_ACTIVITY:
- processMrdtResult(requestCode, resultCode, intent);
- return;
- case DISCLOSURE_LOCATION_ACTIVITY:
- processLocationPermissionResult(resultCode);
+ processMrdtResult(requestCode, intent);
return;
case CHT_EXTERNAL_APP_ACTIVITY:
processChtExternalAppResult(resultCode, intent);
return;
+ case ACCESS_STORAGE_PERMISSION:
+ processStoragePermissionResult(resultCode, intent);
+ return;
+ case ACCESS_LOCATION_PERMISSION:
+ processLocationPermissionResult(resultCode);
+ return;
default:
trace(this, "onActivityResult() :: no handling for requestCode=%s", requestCode.name());
}
@@ -230,38 +231,6 @@ protected void onStop() {
}
}
- @Override
- public void onRequestPermissionsResult(int requestCd, String[] permissions, int[] grantResults) {
- Optional requestCodeOpt = RequestCode.valueOf(requestCd);
-
- if (!requestCodeOpt.isPresent()) {
- trace(this, "onRequestPermissionsResult() :: no handling for requestCode=%s", requestCd);
- return;
- }
-
- RequestCode requestCode = requestCodeOpt.get();
- super.onRequestPermissionsResult(requestCd, permissions, grantResults);
- boolean granted = grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED;
-
- if (requestCode == RequestCode.ACCESS_LOCATION_PERMISSION) {
- if (granted) {
- locationRequestResolved();
- return;
- }
- processGeolocationDeniedStatus();
- return;
- }
-
- if (requestCode == RequestCode.ACCESS_STORAGE_PERMISSION) {
- if (granted) {
- this.chtExternalAppHandler.resumeActivity();
- return;
- }
- trace(this, "ChtExternalAppHandler :: User rejected permission.");
- return;
- }
- }
-
//> ACCESSORS
MrdtSupport getMrdtSupport() {
return this.mrdt;
@@ -271,7 +240,7 @@ SmsSender getSmsSender() {
return this.smsSender;
}
- ChtExternalAppHandler getChtExternalAppLauncherActivity() {
+ ChtExternalAppHandler getChtExternalAppHandler() {
return this.chtExternalAppHandler;
}
@@ -321,47 +290,63 @@ public boolean getLocationPermissions() {
}
trace(this, "getLocationPermissions() :: location not granted before, requesting access...");
- Intent intent = new Intent(this, RequestPermissionActivity.class);
- startActivityForResult(intent, RequestCode.DISCLOSURE_LOCATION_ACTIVITY.getCode());
+ startActivityForResult(
+ new Intent(this, RequestLocationPermissionActivity.class),
+ RequestCode.ACCESS_LOCATION_PERMISSION.getCode()
+ );
return false;
}
- public void locationRequestResolved() {
+//> PRIVATE HELPERS
+ private void locationRequestResolved() {
evaluateJavascript("window.CHTCore.AndroidApi.v1.locationPermissionRequestResolved();");
}
-//> PRIVATE HELPERS
+ private void processLocationPermissionResult(int resultCode) {
+ if (resultCode != RESULT_OK) {
+ try {
+ settings.setUserDeniedGeolocation();
+ } catch (SettingsException e) {
+ error(e, "processLocationPermissionResult :: Error recording negative to access location.");
+ }
+ }
+
+ locationRequestResolved();
+ }
+
private void processChtExternalAppResult(int resultCode, Intent intentData) {
String script = this.chtExternalAppHandler.processResult(resultCode, intentData);
trace(this, "ChtExternalAppHandler :: Executing JavaScript: %s", script);
evaluateJavascript(script);
}
- private void processMrdtResult(RequestCode requestCode, int resultCode, Intent intent) {
+ private void processMrdtResult(RequestCode requestCode, Intent intent) {
String js = mrdt.process(requestCode, intent);
trace(this, "Executing JavaScript: %s", js);
evaluateJavascript(js);
}
- private void processLocationPermissionResult(int resultCode) {
- if (resultCode == RESULT_OK) {
- ActivityCompat.requestPermissions(
- this,
- LOCATION_PERMISSIONS,
- RequestCode.ACCESS_LOCATION_PERMISSION.getCode()
- );
- } else if (resultCode == RESULT_CANCELED) {
- processGeolocationDeniedStatus();
+ private void processStoragePermissionResult(int resultCode, Intent intent) {
+ String triggerClass = intent == null ? null : intent.getStringExtra(RequestStoragePermissionActivity.TRIGGER_CLASS);
+
+ if (FilePickerHandler.class.getName().equals(triggerClass)) {
+ trace(this, "EmbeddedBrowserActivity :: Resuming FilePickerHandler process. Trigger:%s", triggerClass);
+ this.filePickerHandler.resumeProcess(resultCode);
+ return;
}
- }
- private void processGeolocationDeniedStatus() {
- try {
- settings.setUserDeniedGeolocation();
- locationRequestResolved();
- } catch (SettingsException e) {
- error(e, "LocationPermissionRequest :: Error recording negative to access location");
+ if (ChtExternalAppHandler.class.getName().equals(triggerClass)) {
+ trace(this, "EmbeddedBrowserActivity :: Resuming ChtExternalAppHandler activity. Trigger:%s", triggerClass);
+ this.chtExternalAppHandler.resumeActivity(resultCode);
+ return;
}
+
+ trace(
+ this,
+ "EmbeddedBrowserActivity :: No handling for trigger: %s, requestCode: %s",
+ triggerClass,
+ RequestCode.ACCESS_STORAGE_PERMISSION.name()
+ );
}
private void configureUserAgent() {
@@ -454,9 +439,8 @@ public enum RequestCode {
ACCESS_LOCATION_PERMISSION(100),
ACCESS_STORAGE_PERMISSION(101),
CHT_EXTERNAL_APP_ACTIVITY(102),
- DISCLOSURE_LOCATION_ACTIVITY(103),
- GRAB_MRDT_PHOTO_ACTIVITY(104),
- FILE_PICKER_ACTIVITY(105);
+ GRAB_MRDT_PHOTO_ACTIVITY(103),
+ FILE_PICKER_ACTIVITY(104);
private final int requestCode;
diff --git a/src/main/java/org/medicmobile/webapp/mobile/FilePickerHandler.java b/src/main/java/org/medicmobile/webapp/mobile/FilePickerHandler.java
index 5c8d38d8..7e5d7791 100644
--- a/src/main/java/org/medicmobile/webapp/mobile/FilePickerHandler.java
+++ b/src/main/java/org/medicmobile/webapp/mobile/FilePickerHandler.java
@@ -1,6 +1,8 @@
package org.medicmobile.webapp.mobile;
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.provider.MediaStore.ACTION_IMAGE_CAPTURE;
import static android.provider.MediaStore.ACTION_VIDEO_CAPTURE;
import static android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION;
@@ -16,6 +18,7 @@
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient.FileChooserParams;
+import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import java.io.File;
@@ -26,6 +29,7 @@ public class FilePickerHandler {
private final Activity context;
private ValueCallback filePickerCallback;
+ private String[] mimeTypes;
private File tempFile;
public FilePickerHandler(Activity context) {
@@ -41,10 +45,13 @@ void setTempFile(File tempFile) {
}
void openPicker(FileChooserParams fileChooserParams, ValueCallback filePickerCallback) {
- trace(this, "FilePickerHandler :: Start file capture activity");
setFilePickerCallback(filePickerCallback);
setTempFile(null);
- startFileCaptureActivity(fileChooserParams);
+ setMimeTypes(cleanMimeTypes(fileChooserParams));
+
+ if (checkPermissions()) {
+ startFileCaptureActivity();
+ }
}
void processResult(int resultCode, Intent intent) {
@@ -59,7 +66,35 @@ void processResult(int resultCode, Intent intent) {
sendDataToWebapp(intent);
}
+ void resumeProcess(int resultCode) {
+ if (resultCode == RESULT_OK) {
+ startFileCaptureActivity();
+ return;
+ }
+
+ // Giving control back to WebView without files.
+ sendDataToWebapp(null);
+ }
+
//> PRIVATE
+ private void setMimeTypes(String[] mimeTypes) {
+ this.mimeTypes = mimeTypes;
+ }
+
+ private boolean checkPermissions() {
+ if (ContextCompat.checkSelfPermission(this.context, READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
+ return true;
+ }
+
+ trace(this, "FilePickerHandler :: Requesting permissions.");
+ Intent intent = new Intent(this.context, RequestStoragePermissionActivity.class);
+ intent.putExtra(
+ RequestStoragePermissionActivity.TRIGGER_CLASS,
+ FilePickerHandler.class.getName()
+ );
+ this.context.startActivityForResult(intent, RequestCode.ACCESS_STORAGE_PERMISSION.getCode());
+ return false;
+ }
/**
* Executes the file picker callback.
@@ -97,22 +132,21 @@ private Optional getSelectedFileUri(Intent intent) {
return uri;
}
- private void startFileCaptureActivity(FileChooserParams fileChooserParams) {
- String[] mimeTypes = cleanMimeTypes(fileChooserParams);
- trace(this, "FilePickerHandler :: Accepted MIME types: %s", Arrays.toString(mimeTypes));
+ private void startFileCaptureActivity() {
+ trace(this, "FilePickerHandler :: Accepted MIME types: %s", Arrays.toString(this.mimeTypes));
Intent pickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickerIntent.addCategory(Intent.CATEGORY_OPENABLE);
pickerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
Intent captureIntent = null;
- if (mimeTypes.length == 1) {
- String mimeType = mimeTypes[0];
+ if (this.mimeTypes.length == 1) {
+ String mimeType = this.mimeTypes[0];
pickerIntent.setType(mimeType);
captureIntent = getCaptureIntent(mimeType);
} else {
pickerIntent.setType("*/*");
- pickerIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
+ pickerIntent.putExtra(Intent.EXTRA_MIME_TYPES, this.mimeTypes);
}
Intent chooserIntent = Intent.createChooser(pickerIntent, this.context.getString(R.string.promptChooseFile));
diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java
index 253883ab..eb087d30 100644
--- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java
+++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java
@@ -59,7 +59,7 @@ public MedicAndroidJavascript(EmbeddedBrowserActivity parent) {
this.parent = parent;
this.mrdt = parent.getMrdtSupport();
this.smsSender = parent.getSmsSender();
- this.chtExternalAppHandler = parent.getChtExternalAppLauncherActivity();
+ this.chtExternalAppHandler = parent.getChtExternalAppHandler();
}
public void setAlert(Alert soundAlert) {
diff --git a/src/main/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivity.java b/src/main/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivity.java
new file mode 100644
index 00000000..916d3f3f
--- /dev/null
+++ b/src/main/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivity.java
@@ -0,0 +1,108 @@
+package org.medicmobile.webapp.mobile;
+
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static org.medicmobile.webapp.mobile.MedicLog.trace;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Shows a confirmation view that displays a "prominent" disclosure about how
+ * the user geolocation data is used, asking to confirm whether to allow the app to
+ * access the location or not.
+ *
+ * If the user accepts, a request to the API to access the location is made by the main activity,
+ * but Android will show another confirmation dialog. If the user decline the first
+ * confirmation, the request to the API is omitted and the decision recorded to avoid
+ * requesting the same next time.
+ */
+public class RequestLocationPermissionActivity extends FragmentActivity {
+ static final String[] LOCATION_PERMISSIONS = { ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION };
+
+ private final ActivityResultLauncher requestPermissionLauncher =
+ registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), grantedMap -> {
+ boolean allGranted = grantedMap.size() > 0 && !grantedMap.containsValue(false);
+
+ if (allGranted) {
+ trace(this, "RequestLocationPermissionActivity :: User allowed location permission.");
+ setResult(RESULT_OK);
+ finish();
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ boolean rationalFineLocation = !shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION);
+ boolean rationalCoarseLocation = !shouldShowRequestPermissionRationale(ACCESS_COARSE_LOCATION);
+
+ if (rationalFineLocation || rationalCoarseLocation) {
+ trace(
+ this,
+ "RequestLocationPermissionActivity :: User rejected location permission twice or has selected \"never ask again\"." +
+ " Sending user to the app's setting to manually grant the permission."
+ );
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.fromParts("package", getPackageName(), null));
+ this.appSettingsLauncher.launch(intent);
+ return;
+ }
+ }
+
+ trace(this, "RequestLocationPermissionActivity :: User rejected location permission.");
+ setResult(RESULT_CANCELED);
+ finish();
+ });
+
+ private final ActivityResultLauncher appSettingsLauncher =
+ registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+ boolean hasFineLocation = ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED;
+ boolean hasCoarseLocation = ContextCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED;
+
+ if (hasFineLocation && hasCoarseLocation) {
+ trace(this, "RequestLocationPermissionActivity :: User granted location permission from app's settings.");
+ setResult(RESULT_OK);
+ finish();
+ return;
+ }
+
+ trace(this, "RequestLocationPermissionActivity :: User didn't grant location permission from app's settings.");
+ setResult(RESULT_CANCELED);
+ finish();
+ });
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ this.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.request_location_permission);
+
+ String appName = getResources().getString(R.string.app_name);
+ String message = getResources().getString(R.string.locRequestMessage);
+ TextView field = findViewById(R.id.locMessageText);
+ field.setText(String.format(message, appName));
+ }
+
+ public void onClickOk(View view) {
+ trace(this, "RequestLocationPermissionActivity :: User agree with prominent disclosure message.");
+ requestPermissionLauncher.launch(LOCATION_PERMISSIONS);
+ }
+
+ public void onClickNegative(View view) {
+ trace(this, "RequestLocationPermissionActivity :: User disagree with prominent disclosure message.");
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+}
diff --git a/src/main/java/org/medicmobile/webapp/mobile/RequestPermissionActivity.java b/src/main/java/org/medicmobile/webapp/mobile/RequestPermissionActivity.java
deleted file mode 100644
index 957224ff..00000000
--- a/src/main/java/org/medicmobile/webapp/mobile/RequestPermissionActivity.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.medicmobile.webapp.mobile;
-
-import android.os.Bundle;
-import android.view.View;
-import android.view.Window;
-import android.widget.TextView;
-
-import static org.medicmobile.webapp.mobile.MedicLog.trace;
-
-/**
- * Shows a confirmation view that displays a "prominent" disclosure about how
- * the user geolocation data is used, asking to confirm whether to allow the app to
- * access the location or not.
- *
- * If the user accepts, a request to the API to access the location is made by the main activity,
- * but Android will show another confirmation dialog. If the user decline the first
- * confirmation, the request to the API is omitted and the decision recorded to avoid
- * requesting the same next time.
- */
-public class RequestPermissionActivity extends LockableActivity {
-
- @Override public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- this.requestWindowFeature(Window.FEATURE_NO_TITLE);
- setContentView(R.layout.request_permission);
- String message = getResources().getString(R.string.locRequestMessage);
- String appName = getResources().getString(R.string.app_name);
- TextView field = (TextView) findViewById(R.id.locMessageText);
- field.setText(String.format(message, appName));
- }
-
- public void onClickOk(View view) {
- trace(this, "onClickOk() :: user accepted to share the location");
- setResult(RESULT_OK);
- finish();
- }
-
- public void onClickNegative(View view) {
- trace(this, ":: onClickNegative() :: user denied to share the location");
- setResult(RESULT_CANCELED);
- finish();
- }
-}
diff --git a/src/main/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivity.java b/src/main/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivity.java
new file mode 100644
index 00000000..77080521
--- /dev/null
+++ b/src/main/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivity.java
@@ -0,0 +1,104 @@
+package org.medicmobile.webapp.mobile;
+
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static org.medicmobile.webapp.mobile.MedicLog.trace;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
+
+public class RequestStoragePermissionActivity extends FragmentActivity {
+ /**
+ * TRIGGER_CLASS {String} Extra in the request intent to specify the class that trigger this activity,
+ * it will be passed on to the result intent. It can be used to continue the
+ * action of the trigger class after the intent is resolved.
+ */
+ static final String TRIGGER_CLASS = "TRIGGER_CLASS";
+
+ private final ActivityResultLauncher requestPermissionLauncher =
+ registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
+ Intent responseIntent = createResponseIntent();
+
+ if (isGranted) {
+ trace(this, "RequestStoragePermissionActivity :: User allowed storage permission.");
+ setResult(RESULT_OK, responseIntent);
+ finish();
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE)) {
+ trace(
+ this,
+ "RequestStoragePermissionActivity :: User rejected storage permission twice or has selected \"never ask again\"." +
+ " Sending user to the app's setting to manually grant the permission."
+ );
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.fromParts("package", getPackageName(), null));
+ this.appSettingsLauncher.launch(intent);
+ return;
+ }
+
+ trace(this, "RequestStoragePermissionActivity :: User rejected storage permission.");
+ setResult(RESULT_CANCELED, responseIntent);
+ finish();
+ });
+
+ private final ActivityResultLauncher appSettingsLauncher =
+ registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+ Intent responseIntent = createResponseIntent();
+
+ if (ContextCompat.checkSelfPermission(this, READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
+ trace(this, "RequestStoragePermissionActivity :: User granted storage permission from app's settings.");
+ setResult(RESULT_OK, responseIntent);
+ finish();
+ return;
+ }
+
+ trace(this, "RequestStoragePermissionActivity :: User didn't grant storage permission from app's settings.");
+ setResult(RESULT_CANCELED, responseIntent);
+ finish();
+ });
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ this.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.request_storage_permission);
+
+ String appName = getResources().getString(R.string.app_name);
+ String message = getResources().getString(R.string.storageRequestMessage);
+ TextView field = findViewById(R.id.storageMessageText);
+ field.setText(String.format(message, appName));
+ }
+
+ public void onClickAllow(View view) {
+ trace(this, "RequestStoragePermissionActivity :: User agree with prominent disclosure message.");
+ this.requestPermissionLauncher.launch(READ_EXTERNAL_STORAGE);
+ }
+
+ public void onClickDeny(View view) {
+ trace(this, "RequestStoragePermissionActivity :: User disagree with prominent disclosure message.");
+ setResult(RESULT_CANCELED, createResponseIntent());
+ finish();
+ }
+
+ private Intent createResponseIntent() {
+ Intent requestIntent = getIntent();
+ String triggerClass = requestIntent == null ? null : requestIntent.getStringExtra(this.TRIGGER_CLASS);
+ Intent responseIntent = new Intent();
+ responseIntent.putExtra(this.TRIGGER_CLASS, triggerClass);
+ return responseIntent;
+ }
+}
diff --git a/src/main/res/drawable-hdpi/storage.png b/src/main/res/drawable-hdpi/storage.png
new file mode 100644
index 00000000..40a7686e
Binary files /dev/null and b/src/main/res/drawable-hdpi/storage.png differ
diff --git a/src/main/res/drawable-mdpi/storage.png b/src/main/res/drawable-mdpi/storage.png
new file mode 100644
index 00000000..38542ec8
Binary files /dev/null and b/src/main/res/drawable-mdpi/storage.png differ
diff --git a/src/main/res/drawable-xhdpi/storage.png b/src/main/res/drawable-xhdpi/storage.png
new file mode 100644
index 00000000..74a6cf68
Binary files /dev/null and b/src/main/res/drawable-xhdpi/storage.png differ
diff --git a/src/main/res/drawable-xxhdpi/storage.png b/src/main/res/drawable-xxhdpi/storage.png
new file mode 100644
index 00000000..1a4cc3ae
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/storage.png differ
diff --git a/src/main/res/layout/request_permission.xml b/src/main/res/layout/request_location_permission.xml
similarity index 100%
rename from src/main/res/layout/request_permission.xml
rename to src/main/res/layout/request_location_permission.xml
diff --git a/src/main/res/layout/request_storage_permission.xml b/src/main/res/layout/request_storage_permission.xml
new file mode 100644
index 00000000..e6b37aa4
--- /dev/null
+++ b/src/main/res/layout/request_storage_permission.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 662187b0..200d23f7 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -51,6 +51,12 @@
No thanks
My location icon
+ Storage access
+ %s uploads files into reports to analyze and improve health outcomes in your area. To permit storage access select \"Allow\", and then grant storage access in the next prompt.
+ Allow
+ Deny
+ My storage icon
+
Loading…
Upgrading your app
You\'re upgrading to the latest version. Please wait while your data is being migrated.
diff --git a/src/test/java/org/medicmobile/webapp/mobile/ChtExternalAppHandlerTest.java b/src/test/java/org/medicmobile/webapp/mobile/ChtExternalAppHandlerTest.java
index 0e88681b..aa60f380 100644
--- a/src/test/java/org/medicmobile/webapp/mobile/ChtExternalAppHandlerTest.java
+++ b/src/test/java/org/medicmobile/webapp/mobile/ChtExternalAppHandlerTest.java
@@ -12,7 +12,6 @@
import android.net.Uri;
import android.os.Bundle;
import androidx.core.content.ContextCompat;
-import androidx.core.app.ActivityCompat;
import static org.junit.Assert.assertEquals;
import static org.medicmobile.webapp.mobile.EmbeddedBrowserActivity.RequestCode;
@@ -31,6 +30,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
@@ -41,7 +41,7 @@
@Config(sdk=28)
public class ChtExternalAppHandlerTest {
@Mock
- Activity mockContext;
+ Activity contextMock;
@Before
public void setup() {
@@ -51,7 +51,7 @@ public void setup() {
@Test
public void processResult_withIntentExtras_returnsScriptCorrectly() {
//> GIVEN
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
Bundle secondLevelExtras = new Bundle();
secondLevelExtras.putString("id", "abc-1234");
@@ -95,7 +95,7 @@ public void processResult_withIntentExtras_returnsScriptCorrectly() {
@Test
public void processResult_withoutIntentExtras_returnsScriptCorrectly() {
//> GIVEN
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
Intent intent = mock(Intent.class);
when(intent.getExtras()).thenReturn(null);
@@ -119,7 +119,7 @@ public void processResult_withoutIntentExtras_returnsScriptCorrectly() {
@Test
public void processResult_withException_catchesException() {
//> GIVEN
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
Intent intent = mock(Intent.class);
when(intent.getExtras()).thenThrow(NullPointerException.class);
@@ -136,7 +136,7 @@ public void processResult_withBadResultCode_logError() {
try (MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
//> GIVEN
Intent intent = mock(Intent.class);
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
String expectedMessageWarn = "ChtExternalAppHandler :: Bad result code: %s. The external app either: " +
"explicitly returned this result, didn't return any result or crashed during the operation.";
String expectedMessageConsole = "ChtExternalAppHandler :: Bad result code: " + RESULT_CANCELED + ". The external app either: " +
@@ -155,8 +155,8 @@ public void processResult_withBadResultCode_logError() {
public void startIntent_withValidIntent_startsIntentCorrectly() {
//> GIVEN
ChtExternalApp chtExternalApp = mock(ChtExternalApp.class);
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
- doNothing().when(mockContext).startActivityForResult(any(), anyInt());
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
+ doNothing().when(contextMock).startActivityForResult(any(), anyInt());
Intent intent = new Intent();
intent.setAction("an.action");
@@ -174,7 +174,7 @@ public void startIntent_withValidIntent_startsIntentCorrectly() {
//> THEN
verify(chtExternalApp).createIntent();
- verify(mockContext).startActivityForResult(eq(intent), eq(RequestCode.CHT_EXTERNAL_APP_ACTIVITY.getCode()));
+ verify(contextMock).startActivityForResult(eq(intent), eq(RequestCode.CHT_EXTERNAL_APP_ACTIVITY.getCode()));
}
@Test
@@ -182,8 +182,8 @@ public void startIntent_withException_catchesException() {
try (MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
//> GIVEN
ChtExternalApp chtExternalApp = mock(ChtExternalApp.class);
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
- doThrow(ActivityNotFoundException.class).when(mockContext).startActivityForResult(any(), anyInt());
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
+ doThrow(ActivityNotFoundException.class).when(contextMock).startActivityForResult(any(), anyInt());
Intent intent = new Intent();
intent.setAction("an.action");
@@ -197,7 +197,7 @@ public void startIntent_withException_catchesException() {
//> THEN
verify(chtExternalApp).createIntent();
- verify(mockContext).startActivityForResult(eq(intent), eq(RequestCode.CHT_EXTERNAL_APP_ACTIVITY.getCode()));
+ verify(contextMock).startActivityForResult(eq(intent), eq(RequestCode.CHT_EXTERNAL_APP_ACTIVITY.getCode()));
medicLogMock.verify(() -> MedicLog.error(
any(),
eq("ChtExternalAppHandler :: Error when starting the activity %s %s"),
@@ -210,14 +210,11 @@ public void startIntent_withException_catchesException() {
@Test
public void startIntent_withoutStoragePermissions_requestsPermissions() {
- try (
- MockedStatic activityCompatMock = mockStatic(ActivityCompat.class);
- MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
- ) {
+ try (MockedStatic contextCompatMock = mockStatic(ContextCompat.class)) {
//> GIVEN
ChtExternalApp chtExternalApp = mock(ChtExternalApp.class);
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
- doNothing().when(mockContext).startActivityForResult(any(), anyInt());
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
+ doNothing().when(contextMock).startActivityForResult(any(), anyInt());
when(chtExternalApp.createIntent()).thenReturn(new Intent());
contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), anyString())).thenReturn(PERMISSION_DENIED);
@@ -226,27 +223,30 @@ public void startIntent_withoutStoragePermissions_requestsPermissions() {
//> THEN
verify(chtExternalApp).createIntent();
- contextCompatMock.verify(() -> ContextCompat.checkSelfPermission(mockContext, READ_EXTERNAL_STORAGE));
- activityCompatMock.verify(() -> ActivityCompat.requestPermissions(
- mockContext,
- new String[]{READ_EXTERNAL_STORAGE},
- RequestCode.ACCESS_STORAGE_PERMISSION.getCode()
- ));
- verify(mockContext, never()).startActivityForResult(any(), anyInt());
+ contextCompatMock.verify(() -> ContextCompat.checkSelfPermission(contextMock, READ_EXTERNAL_STORAGE));
+ verify(contextMock, never()).startActivityForResult(any(), eq(RequestCode.CHT_EXTERNAL_APP_ACTIVITY.getCode()));
+ ArgumentCaptor argument = ArgumentCaptor.forClass(Intent.class);
+ verify(contextMock).startActivityForResult(
+ argument.capture(),
+ eq(RequestCode.ACCESS_STORAGE_PERMISSION.getCode())
+ );
+ Intent requestStorageIntent = argument.getValue();
+ assertEquals(RequestStoragePermissionActivity.class.getName(), requestStorageIntent.getComponent().getClassName());
+ assertEquals(
+ ChtExternalAppHandler.class.getName(),
+ requestStorageIntent.getStringExtra(RequestStoragePermissionActivity.TRIGGER_CLASS)
+ );
}
}
@Test
public void resumeActivity_withLastIntent_startsIntentCorrectly() {
- try (
- MockedStatic activityCompatMock = mockStatic(ActivityCompat.class);
- MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
- ) {
+ try (MockedStatic contextCompatMock = mockStatic(ContextCompat.class)) {
//> GIVEN
ChtExternalApp chtExternalApp = mock(ChtExternalApp.class);
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
- doNothing().when(mockContext).startActivityForResult(any(), anyInt());
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
+ doNothing().when(contextMock).startActivityForResult(any(), anyInt());
contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), anyString())).thenReturn(PERMISSION_DENIED);
Intent intent = new Intent();
@@ -259,31 +259,50 @@ public void resumeActivity_withLastIntent_startsIntentCorrectly() {
chtExternalAppHandler.startIntent(chtExternalApp);
//> WHEN
- chtExternalAppHandler.resumeActivity();
+ chtExternalAppHandler.resumeActivity(RESULT_OK);
//> THEN
verify(chtExternalApp).createIntent();
- contextCompatMock.verify(() -> ContextCompat.checkSelfPermission(mockContext, READ_EXTERNAL_STORAGE));
- activityCompatMock.verify(() -> ActivityCompat.requestPermissions(
- mockContext,
- new String[]{READ_EXTERNAL_STORAGE},
- RequestCode.ACCESS_STORAGE_PERMISSION.getCode()
- ));
- verify(mockContext).startActivityForResult(eq(intent), eq(RequestCode.CHT_EXTERNAL_APP_ACTIVITY.getCode()));
+ contextCompatMock.verify(() -> ContextCompat.checkSelfPermission(contextMock, READ_EXTERNAL_STORAGE));
+ verify(contextMock).startActivityForResult(eq(intent), eq(RequestCode.CHT_EXTERNAL_APP_ACTIVITY.getCode()));
+ ArgumentCaptor argument = ArgumentCaptor.forClass(Intent.class);
+ verify(contextMock).startActivityForResult(
+ argument.capture(),
+ eq(RequestCode.ACCESS_STORAGE_PERMISSION.getCode())
+ );
+ Intent requestStorageIntent = argument.getValue();
+ assertEquals(RequestStoragePermissionActivity.class.getName(), requestStorageIntent.getComponent().getClassName());
+ assertEquals(
+ ChtExternalAppHandler.class.getName(),
+ requestStorageIntent.getStringExtra(RequestStoragePermissionActivity.TRIGGER_CLASS)
+ );
}
}
@Test
public void resumeActivity_withoutIntent_doesNothing() {
//> GIVEN
- ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(mockContext);
- doNothing().when(mockContext).startActivityForResult(any(), anyInt());
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
+ doNothing().when(contextMock).startActivityForResult(any(), anyInt());
+
+ //> WHEN
+ chtExternalAppHandler.resumeActivity(RESULT_OK);
+
+ //> THEN
+ verify(contextMock, never()).startActivityForResult(any(), anyInt());
+ }
+
+ @Test
+ public void resumeActivity_withBadResult_doesNothing() {
+ //> GIVEN
+ ChtExternalAppHandler chtExternalAppHandler = new ChtExternalAppHandler(contextMock);
+ doNothing().when(contextMock).startActivityForResult(any(), anyInt());
//> WHEN
- chtExternalAppHandler.resumeActivity();
+ chtExternalAppHandler.resumeActivity(RESULT_CANCELED);
//> THEN
- verify(mockContext, never()).startActivityForResult(any(), anyInt());
+ verify(contextMock, never()).startActivityForResult(any(), anyInt());
}
}
diff --git a/src/test/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivityTest.java b/src/test/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivityTest.java
index 2e2357e2..785a8c26 100644
--- a/src/test/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivityTest.java
+++ b/src/test/java/org/medicmobile/webapp/mobile/EmbeddedBrowserActivityTest.java
@@ -9,15 +9,16 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.medicmobile.webapp.mobile.EmbeddedBrowserActivity.RequestCode;
-import static org.medicmobile.webapp.mobile.EmbeddedBrowserActivity.LOCATION_PERMISSIONS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.content.Intent;
-import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.intent.matcher.IntentMatchers;
@@ -90,7 +91,7 @@ public void getLocationPermissions_withPermissionsDenied_returnsFalse() {
assertFalse(embeddedBrowserActivity.getLocationPermissions());
- Intents.intended(IntentMatchers.hasComponent(RequestPermissionActivity.class.getName()));
+ Intents.intended(IntentMatchers.hasComponent(RequestLocationPermissionActivity.class.getName()));
medicLogMock.verify(() -> MedicLog.trace(
eq(embeddedBrowserActivity),
eq("getLocationPermissions() :: location not granted before, requesting access...")
@@ -102,10 +103,10 @@ public void getLocationPermissions_withPermissionsDenied_returnsFalse() {
}
@Test
- public void getLocationPermissions_withPermissionsDenied_requestPermissions() {
+ public void getLocationPermissions_withPermissionsAlreadyDenied_returnsFalse() {
try(
MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
- MockedStatic activityCompatMock = mockStatic(ActivityCompat.class);
+ MockedStatic medicLogMock = mockStatic(MedicLog.class);
) {
contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), eq(ACCESS_FINE_LOCATION))).thenReturn(PERMISSION_DENIED);
contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), eq(ACCESS_COARSE_LOCATION))).thenReturn(PERMISSION_DENIED);
@@ -116,16 +117,16 @@ public void getLocationPermissions_withPermissionsDenied_requestPermissions() {
Intents.init();
assertFalse(embeddedBrowserActivity.getLocationPermissions());
- Intents.intended(IntentMatchers.hasComponent(RequestPermissionActivity.class.getName()));
+ Intents.intended(IntentMatchers.hasComponent(RequestLocationPermissionActivity.class.getName()));
ShadowActivity shadowActivity = shadowOf(embeddedBrowserActivity);
Intent requestIntent = shadowActivity.peekNextStartedActivityForResult().intent;
- shadowActivity.receiveResult(requestIntent, RESULT_OK, new Intent());
+ shadowActivity.receiveResult(requestIntent, RESULT_CANCELED, new Intent());
- activityCompatMock.verify(() -> ActivityCompat.requestPermissions(
- embeddedBrowserActivity,
- LOCATION_PERMISSIONS,
- RequestCode.ACCESS_LOCATION_PERMISSION.getCode()
+ assertFalse(embeddedBrowserActivity.getLocationPermissions());
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("getLocationPermissions() :: user has previously denied to share location")
));
Intents.release();
@@ -134,30 +135,145 @@ public void getLocationPermissions_withPermissionsDenied_requestPermissions() {
}
@Test
- public void getLocationPermissions_withPermissionsAlreadyDenied_returnsFalse() {
- try(
- MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
- MockedStatic medicLogMock = mockStatic(MedicLog.class);
- ) {
- contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), eq(ACCESS_FINE_LOCATION))).thenReturn(PERMISSION_DENIED);
- contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), eq(ACCESS_COARSE_LOCATION))).thenReturn(PERMISSION_DENIED);
+ public void processStoragePermissionResult_withResponseIntent_resumeCHTExternalApp(){
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ scenarioRule
+ .getScenario()
+ .onActivity(embeddedBrowserActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ Intent responseIntent = mock(Intent.class);
+ when(responseIntent.getStringExtra(RequestStoragePermissionActivity.TRIGGER_CLASS))
+ .thenReturn(ChtExternalAppHandler.class.getName());
+
+ //> WHEN
+ embeddedBrowserActivity.startActivityForResult(new Intent(), RequestCode.ACCESS_STORAGE_PERMISSION.getCode());
+
+ //> THEN
+ ShadowActivity shadowActivity = shadowOf(embeddedBrowserActivity);
+ Intent requestIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+
+ shadowActivity.receiveResult(requestIntent, RESULT_CANCELED, responseIntent);
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("EmbeddedBrowserActivity :: Resuming ChtExternalAppHandler activity. Trigger:%s"),
+ eq(ChtExternalAppHandler.class.getName())
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("EmbeddedBrowserActivity :: No handling for trigger: %s, requestCode:"),
+ eq(null),
+ eq(RequestCode.ACCESS_STORAGE_PERMISSION.name())
+ ), never());
+
+ Intents.release();
+ });
+ }
+ }
+ @Test
+ public void processStoragePermissionResult_withResponseIntent_resumeFilePickerHander(){
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
scenarioRule
.getScenario()
.onActivity(embeddedBrowserActivity -> {
Intents.init();
- assertFalse(embeddedBrowserActivity.getLocationPermissions());
- Intents.intended(IntentMatchers.hasComponent(RequestPermissionActivity.class.getName()));
+ //> GIVEN
+ Intent responseIntent = mock(Intent.class);
+ when(responseIntent.getStringExtra(RequestStoragePermissionActivity.TRIGGER_CLASS))
+ .thenReturn(FilePickerHandler.class.getName());
+
+ //> WHEN
+ embeddedBrowserActivity.startActivityForResult(new Intent(), RequestCode.ACCESS_STORAGE_PERMISSION.getCode());
+ //> THEN
ShadowActivity shadowActivity = shadowOf(embeddedBrowserActivity);
Intent requestIntent = shadowActivity.peekNextStartedActivityForResult().intent;
- shadowActivity.receiveResult(requestIntent, RESULT_CANCELED, new Intent());
- assertFalse(embeddedBrowserActivity.getLocationPermissions());
+ shadowActivity.receiveResult(requestIntent, RESULT_CANCELED, responseIntent);
medicLogMock.verify(() -> MedicLog.trace(
eq(embeddedBrowserActivity),
- eq("getLocationPermissions() :: user has previously denied to share location")
+ eq("EmbeddedBrowserActivity :: Resuming FilePickerHandler process. Trigger:%s"),
+ eq(FilePickerHandler.class.getName())
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("EmbeddedBrowserActivity :: No handling for trigger: %s, requestCode:"),
+ eq(null),
+ eq(RequestCode.ACCESS_STORAGE_PERMISSION.name())
+ ), never());
+
+ Intents.release();
+ });
+ }
+ }
+
+ @Test
+ public void processStoragePermissionResult_withoutResponseIntent_doesntResumeProcess(){
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ scenarioRule
+ .getScenario()
+ .onActivity(embeddedBrowserActivity -> {
+ Intents.init();
+
+ //> WHEN
+ embeddedBrowserActivity.startActivityForResult(new Intent(), RequestCode.ACCESS_STORAGE_PERMISSION.getCode());
+
+ //> THEN
+ ShadowActivity shadowActivity = shadowOf(embeddedBrowserActivity);
+ Intent requestIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+
+ shadowActivity.receiveResult(requestIntent, RESULT_CANCELED, null);
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("EmbeddedBrowserActivity :: Resuming FilePickerHandler process. Trigger:%s"),
+ eq(FilePickerHandler.class.getName())
+ ), never());
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("EmbeddedBrowserActivity :: Resuming ChtExternalAppHandler activity. Trigger:%s"),
+ eq(ChtExternalAppHandler.class.getName())
+ ), never());
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("EmbeddedBrowserActivity :: No handling for trigger: %s, requestCode: %s"),
+ eq(null),
+ eq(RequestCode.ACCESS_STORAGE_PERMISSION.name())
+ ));
+
+ Intents.release();
+ });
+ }
+ }
+
+ @Test
+ public void onActivityResult_unknownRequestCode_logRequestCode(){
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ scenarioRule
+ .getScenario()
+ .onActivity(embeddedBrowserActivity -> {
+ Intents.init();
+
+ //> WHEN
+ embeddedBrowserActivity.startActivityForResult(new Intent(), 123456789);
+
+ //> THEN
+ ShadowActivity shadowActivity = shadowOf(embeddedBrowserActivity);
+ Intent requestIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+
+ shadowActivity.receiveResult(requestIntent, RESULT_OK, new Intent());
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("onActivityResult() :: requestCode=%s, resultCode=%s"),
+ any(),
+ any()
+ ), never());
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(embeddedBrowserActivity),
+ eq("onActivityResult() :: no handling for requestCode=%s"),
+ eq(123456789)
));
Intents.release();
diff --git a/src/test/java/org/medicmobile/webapp/mobile/FilePickerHandlerTest.java b/src/test/java/org/medicmobile/webapp/mobile/FilePickerHandlerTest.java
index 01d277bd..a47fe5d0 100644
--- a/src/test/java/org/medicmobile/webapp/mobile/FilePickerHandlerTest.java
+++ b/src/test/java/org/medicmobile/webapp/mobile/FilePickerHandlerTest.java
@@ -1,7 +1,9 @@
package org.medicmobile.webapp.mobile;
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.provider.MediaStore.ACTION_IMAGE_CAPTURE;
import static android.provider.MediaStore.ACTION_VIDEO_CAPTURE;
import static android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION;
@@ -12,8 +14,10 @@
import static org.medicmobile.webapp.mobile.EmbeddedBrowserActivity.RequestCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -32,6 +36,7 @@
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient.FileChooserParams;
+import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import org.junit.Before;
@@ -492,4 +497,87 @@ public void openPicker_withEmptyMimeType_startActivityWithoutCaptureIntent() {
String[] extraMimeTypes = pickerIntent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
assertEquals("[]", Arrays.toString(extraMimeTypes));
}
+
+ @Test
+ public void openPicker_withoutStoragePermission_requestPermission() {
+ try (MockedStatic contextCompatMock = mockStatic(ContextCompat.class)) {
+ //> GIVEN
+ doNothing().when(contextMock).startActivityForResult(any(), anyInt());
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), anyString())).thenReturn(PERMISSION_DENIED);
+
+ FileChooserParams fileChooserParamsMock = mock(FileChooserParams.class);
+ when(fileChooserParamsMock.getAcceptTypes()).thenReturn(new String[]{});
+ ValueCallback filePickerCallbackMock = mock(ValueCallback.class);
+ FilePickerHandler filePickerHandlerMock = spy(new FilePickerHandler(contextMock));
+
+ //> WHEN
+ filePickerHandlerMock.openPicker(fileChooserParamsMock, filePickerCallbackMock);
+
+ //> THEN
+ contextCompatMock.verify(() -> ContextCompat.checkSelfPermission(contextMock, READ_EXTERNAL_STORAGE));
+ verify(contextMock, never()).startActivityForResult(any(), eq(RequestCode.FILE_PICKER_ACTIVITY.getCode()));
+
+ ArgumentCaptor argument = ArgumentCaptor.forClass(Intent.class);
+ verify(contextMock).startActivityForResult(
+ argument.capture(),
+ eq(RequestCode.ACCESS_STORAGE_PERMISSION.getCode())
+ );
+ Intent requestStorageIntent = argument.getValue();
+ assertEquals(RequestStoragePermissionActivity.class.getName(), requestStorageIntent.getComponent().getClassName());
+ assertEquals(
+ FilePickerHandler.class.getName(),
+ requestStorageIntent.getStringExtra(RequestStoragePermissionActivity.TRIGGER_CLASS)
+ );
+ }
+ }
+
+ @Test
+ public void resumeProcess_withResultOk_startActivity() {
+ try (MockedStatic contextCompatMock = mockStatic(ContextCompat.class)) {
+ //> GIVEN
+ stubMethodsInContextMock();
+ ValueCallback filePickerCallbackMock = mock(ValueCallback.class);
+ FilePickerHandler filePickerHandlerMock = spy(new FilePickerHandler(contextMock));
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), anyString())).thenReturn(PERMISSION_DENIED);
+
+ FileChooserParams fileChooserParamsMock = mock(FileChooserParams.class);
+ when(fileChooserParamsMock.getAcceptTypes()).thenReturn(new String[]{"audio/*"});
+ filePickerHandlerMock.openPicker(fileChooserParamsMock, filePickerCallbackMock);
+
+ //> WHEN
+ filePickerHandlerMock.resumeProcess(RESULT_OK);
+
+ //> THEN
+ verify(filePickerHandlerMock).setFilePickerCallback(filePickerCallbackMock);
+ verify(contextMock).startActivityForResult(any(), eq(RequestCode.FILE_PICKER_ACTIVITY.getCode()));
+ }
+ }
+
+ @Test
+ public void resumeProcess_withBadResult_logWarningAndFinishesPicker() {
+ try (
+ MockedStatic contextCompatMock = mockStatic(ContextCompat.class);
+ MockedStatic medicLogMock = mockStatic(MedicLog.class);
+ ) {
+ //> GIVEN
+ ValueCallback filePickerCallbackMock = mock(ValueCallback.class);
+ FilePickerHandler filePickerHandlerMock = spy(new FilePickerHandler(contextMock));
+ contextCompatMock.when(() -> ContextCompat.checkSelfPermission(any(), anyString())).thenReturn(PERMISSION_DENIED);
+
+ FileChooserParams fileChooserParamsMock = mock(FileChooserParams.class);
+ when(fileChooserParamsMock.getAcceptTypes()).thenReturn(new String[]{});
+ filePickerHandlerMock.openPicker(fileChooserParamsMock, filePickerCallbackMock);
+
+ //> WHEN
+ filePickerHandlerMock.resumeProcess(RESULT_CANCELED);
+
+ //> THEN
+ verify(filePickerCallbackMock).onReceiveValue(null);
+ medicLogMock.verify(() -> MedicLog.trace(
+ eq(filePickerHandlerMock),
+ eq("FilePickerHandler :: Sending data back to webapp, URI: %s"),
+ eq("null")
+ ));
+ }
+ }
}
diff --git a/src/test/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivityTest.java b/src/test/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivityTest.java
new file mode 100644
index 00000000..c344f2b5
--- /dev/null
+++ b/src/test/java/org/medicmobile/webapp/mobile/RequestLocationPermissionActivityTest.java
@@ -0,0 +1,260 @@
+package org.medicmobile.webapp.mobile;
+
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+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.junit.Assert.assertNull;
+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.ActivityResult;
+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 RequestLocationPermissionActivityTest {
+
+ @Rule
+ public ActivityScenarioRule scenarioRule = new ActivityScenarioRule<>(RequestLocationPermissionActivity.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(requestLocationPermissionActivity -> {
+ //> GIVEN
+ ShadowActivity shadowActivity = shadowOf(requestLocationPermissionActivity);
+ shadowActivity.grantPermissions(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION);
+
+ //> WHEN
+ requestLocationPermissionActivity.onClickOk(null);
+ });
+ scenario.moveToState(Lifecycle.State.DESTROYED);
+
+ //> THEN
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_OK, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNull(resultIntent);
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User allowed location permission.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withPermissionDenied_setResolveCanceled() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestLocationPermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ ShadowActivity shadowActivity = shadowOf(requestLocationPermissionActivity);
+ packageManager.setShouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION, true);
+ packageManager.setShouldShowRequestPermissionRationale(ACCESS_COARSE_LOCATION, true);
+
+ //> WHEN
+ requestLocationPermissionActivity.onClickOk(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(2, permissions.length);
+ assertEquals(ACCESS_FINE_LOCATION, permissions[0]);
+ assertEquals(ACCESS_COARSE_LOCATION, permissions[1]);
+
+ Intents.release();
+ });
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNull(resultIntent);
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User rejected location permission.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withNeverAskAgainAndPermissionGranted_setResolveOk() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestLocationPermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ String packageName = "package:" + requestLocationPermissionActivity.getPackageName();
+ ShadowActivity shadowActivity = shadowOf(requestLocationPermissionActivity);
+ // Setting "Never ask again" case.
+ packageManager.setShouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION, false);
+ packageManager.setShouldShowRequestPermissionRationale(ACCESS_COARSE_LOCATION, false);
+
+ //> WHEN
+ requestLocationPermissionActivity.onClickOk(null);
+ Intent permissionIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+ shadowActivity.receiveResult(permissionIntent, RESULT_OK, null);
+
+ shadowActivity.grantPermissions(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION);
+ 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();
+ });
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_OK, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNull(resultIntent);
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User rejected location 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(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User granted location permission from app's settings.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withNeverAskAgainAndPermissionDenied_setResolveOk() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestLocationPermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ String packageName = "package:" + requestLocationPermissionActivity.getPackageName();
+ ShadowActivity shadowActivity = shadowOf(requestLocationPermissionActivity);
+ // Setting "Never ask again" case.
+ packageManager.setShouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION, false);
+ packageManager.setShouldShowRequestPermissionRationale(ACCESS_COARSE_LOCATION, false);
+
+ //> WHEN
+ requestLocationPermissionActivity.onClickOk(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();
+ });
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNull(resultIntent);
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User rejected location 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(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: User didn't grant location permission from app's settings.")
+ ));
+ }
+ }
+
+
+ @Test
+ public void onClickNegative_noIntentsStarted_setResolveCanceled() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestLocationPermissionActivity -> {
+ Intents.init();
+ //> WHEN
+ requestLocationPermissionActivity.onClickNegative(null);
+
+ //> THEN
+ assertEquals(0, Intents.getIntents().size());
+
+ Intents.release();
+ });
+ scenario.moveToState(Lifecycle.State.DESTROYED);
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNull(resultIntent);
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestLocationPermissionActivity.class),
+ eq("RequestLocationPermissionActivity :: 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
new file mode 100644
index 00000000..fe17d6ff
--- /dev/null
+++ b/src/test/java/org/medicmobile/webapp/mobile/RequestStoragePermissionActivityTest.java
@@ -0,0 +1,297 @@
+package org.medicmobile.webapp.mobile;
+
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+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.junit.Assert.assertNull;
+import static org.medicmobile.webapp.mobile.RequestStoragePermissionActivity.TRIGGER_CLASS;
+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.ActivityResult;
+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 RequestStoragePermissionActivityTest {
+
+ @Rule
+ public ActivityScenarioRule scenarioRule = new ActivityScenarioRule<>(RequestStoragePermissionActivity.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(requestStoragePermissionActivity -> {
+ //> GIVEN
+ requestStoragePermissionActivity.getIntent().putExtra(TRIGGER_CLASS, "a.trigger.class");
+ ShadowActivity shadowActivity = shadowOf(requestStoragePermissionActivity);
+ shadowActivity.grantPermissions(READ_EXTERNAL_STORAGE);
+
+ //> WHEN
+ requestStoragePermissionActivity.onClickAllow(null);
+ });
+ scenario.moveToState(Lifecycle.State.DESTROYED);
+
+ //> THEN
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_OK, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNotNull(resultIntent);
+ assertEquals("a.trigger.class", resultIntent.getStringExtra(TRIGGER_CLASS));
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User allowed storage permission.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withoutExtras_setTriggerClassNull() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestStoragePermissionActivity -> {
+ //> GIVEN
+ ShadowActivity shadowActivity = shadowOf(requestStoragePermissionActivity);
+ shadowActivity.grantPermissions(READ_EXTERNAL_STORAGE);
+
+ //> WHEN
+ requestStoragePermissionActivity.onClickAllow(null);
+ });
+ scenario.moveToState(Lifecycle.State.DESTROYED);
+
+ //> THEN
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_OK, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNotNull(resultIntent);
+ assertNull(resultIntent.getStringExtra(TRIGGER_CLASS));
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User allowed storage permission.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withPermissionDenied_setResolveCanceled() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestStoragePermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ requestStoragePermissionActivity.getIntent().putExtra(TRIGGER_CLASS, "a.trigger.class");
+ ShadowActivity shadowActivity = shadowOf(requestStoragePermissionActivity);
+ packageManager.setShouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE, true);
+
+ //> WHEN
+ requestStoragePermissionActivity.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(READ_EXTERNAL_STORAGE, permissions[0]);
+
+ Intents.release();
+ });
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNotNull(resultIntent);
+ assertEquals("a.trigger.class", resultIntent.getStringExtra(TRIGGER_CLASS));
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User rejected storage permission.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withNeverAskAgainAndPermissionGranted_setResolveOk() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestStoragePermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ String packageName = "package:" + requestStoragePermissionActivity.getPackageName();
+ requestStoragePermissionActivity.getIntent().putExtra(TRIGGER_CLASS, "a.trigger.class");
+ ShadowActivity shadowActivity = shadowOf(requestStoragePermissionActivity);
+ // Setting "Never ask again" case.
+ packageManager.setShouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE, false);
+
+ //> WHEN
+ requestStoragePermissionActivity.onClickAllow(null);
+ Intent permissionIntent = shadowActivity.peekNextStartedActivityForResult().intent;
+ shadowActivity.receiveResult(permissionIntent, RESULT_OK, null);
+
+ shadowActivity.grantPermissions(READ_EXTERNAL_STORAGE);
+ 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();
+ });
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_OK, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNotNull(resultIntent);
+ assertEquals("a.trigger.class", resultIntent.getStringExtra(TRIGGER_CLASS));
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User rejected storage 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(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User granted storage permission from app's settings.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickAllow_withNeverAskAgainAndPermissionDenied_setResolveOk() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestStoragePermissionActivity -> {
+ Intents.init();
+
+ //> GIVEN
+ String packageName = "package:" + requestStoragePermissionActivity.getPackageName();
+ requestStoragePermissionActivity.getIntent().putExtra(TRIGGER_CLASS, "a.trigger.class");
+ ShadowActivity shadowActivity = shadowOf(requestStoragePermissionActivity);
+ // Setting "Never ask again" case.
+ packageManager.setShouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE, false);
+
+ //> WHEN
+ requestStoragePermissionActivity.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();
+ });
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNotNull(resultIntent);
+ assertEquals("a.trigger.class", resultIntent.getStringExtra(TRIGGER_CLASS));
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User agree with prominent disclosure message.")
+ ));
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User rejected storage 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(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User didn't grant storage permission from app's settings.")
+ ));
+ }
+ }
+
+ @Test
+ public void onClickNegative_noIntentsStarted_setResolveCanceled() {
+ try(MockedStatic medicLogMock = mockStatic(MedicLog.class)) {
+ ActivityScenario scenario = scenarioRule.getScenario();
+
+ scenario.onActivity(requestStoragePermissionActivity -> {
+ Intents.init();
+ //> WHEN
+ requestStoragePermissionActivity.onClickDeny(null);
+
+ //> THEN
+ assertEquals(0, Intents.getIntents().size());
+
+ Intents.release();
+ });
+ scenario.moveToState(Lifecycle.State.DESTROYED);
+
+ ActivityResult result = scenario.getResult();
+ assertEquals(RESULT_CANCELED, result.getResultCode());
+ Intent resultIntent = result.getResultData();
+ assertNotNull(resultIntent);
+ assertNull(resultIntent.getStringExtra(TRIGGER_CLASS));
+
+ medicLogMock.verify(() -> MedicLog.trace(
+ any(RequestStoragePermissionActivity.class),
+ eq("RequestStoragePermissionActivity :: User disagree with prominent disclosure message.")
+ ));
+ }
+ }
+}