Skip to content

Commit

Permalink
148 - Prominent disclosure for storage permission (medic#250)
Browse files Browse the repository at this point in the history
Ticket: medic#148

This commit:
- Adds a prominent disclosure for when requesting storage permission
- Updates location prominent disclosure to use intent's callback contracts.
- Handle "never ask again" or when permission is denied for second time or when the permission is denied from the App's Settings page, at this point we can't request the grant automatically so the user is redirected to the app's settings to manually grant the permission.
  • Loading branch information
latin-panda authored and melema123 committed Apr 27, 2022
1 parent e442417 commit c5251e1
Show file tree
Hide file tree
Showing 21 changed files with 1,244 additions and 196 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 3 additions & 1 deletion src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
android:screenOrientation="portrait"/>
<activity android:name="SettingsDialogActivity"
android:screenOrientation="portrait"/>
<activity android:name="RequestPermissionActivity"
<activity android:name="RequestLocationPermissionActivity"
android:screenOrientation="portrait"/>
<activity android:name="RequestStoragePermissionActivity"
android:screenOrientation="portrait"/>
<activity android:name="AppUrlIntentActivity"
android:launchMode="singleInstance"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
import android.app.Activity;
import android.content.Intent;
import androidx.core.content.ContextCompat;
import androidx.core.app.ActivityCompat;


import org.json.JSONObject;

Expand All @@ -22,7 +20,6 @@
public class ChtExternalAppHandler {

private final Activity context;
private final static String[] PERMISSIONS_STORAGE = { READ_EXTERNAL_STORAGE };
private Intent lastIntent;

ChtExternalAppHandler(Activity context) {
Expand Down Expand Up @@ -53,24 +50,25 @@ String processResult(int resultCode, Intent intent) {
}

void startIntent(ChtExternalApp chtExternalApp) {
Intent intent = chtExternalApp.createIntent();
Intent chtExternalAppIntent = chtExternalApp.createIntent();

if (ContextCompat.checkSelfPermission(this.context, READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
trace(this, "ChtExternalAppHandler :: Requesting storage permissions to process image files taken from external apps");
this.lastIntent = intent; // Saving intent to start it when permission is granted.
ActivityCompat.requestPermissions(
this.context,
PERMISSIONS_STORAGE,
RequestCode.ACCESS_STORAGE_PERMISSION.getCode()
this.lastIntent = chtExternalAppIntent; // Saving intent to start it when permission is granted.
Intent requestStorageIntent = new Intent(this.context, RequestStoragePermissionActivity.class);
requestStorageIntent.putExtra(
RequestStoragePermissionActivity.TRIGGER_CLASS,
ChtExternalAppHandler.class.getName()
);
this.context.startActivityForResult(requestStorageIntent, RequestCode.ACCESS_STORAGE_PERMISSION.getCode());
return;
}

startActivity(intent);
startActivity(chtExternalAppIntent);
}

void resumeActivity() {
if (this.lastIntent == null) {
void resumeActivity(int resultCode) {
if (resultCode != RESULT_OK || this.lastIntent == null) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import android.webkit.WebView;
import android.widget.Toast;

import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import java.util.Arrays;
Expand All @@ -51,8 +50,6 @@ public class EmbeddedBrowserActivity extends LockableActivity {
private ChtExternalAppHandler chtExternalAppHandler;
private boolean isMigrationRunning = false;

static final String[] LOCATION_PERMISSIONS = { ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION };

private static final ValueCallback<String> IGNORE_RESULT = new ValueCallback<String>() {
public void onReceiveValue(String result) { /* ignore */ }
};
Expand Down Expand Up @@ -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<RequestCode> requestCodeOpt = RequestCode.valueOf(requestCd);

if (!requestCodeOpt.isPresent()) {
Expand All @@ -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());
}
Expand All @@ -230,38 +231,6 @@ protected void onStop() {
}
}

@Override
public void onRequestPermissionsResult(int requestCd, String[] permissions, int[] grantResults) {
Optional<RequestCode> 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;
Expand All @@ -271,7 +240,7 @@ SmsSender getSmsSender() {
return this.smsSender;
}

ChtExternalAppHandler getChtExternalAppLauncherActivity() {
ChtExternalAppHandler getChtExternalAppHandler() {
return this.chtExternalAppHandler;
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;

Expand Down
50 changes: 42 additions & 8 deletions src/main/java/org/medicmobile/webapp/mobile/FilePickerHandler.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -26,6 +29,7 @@ public class FilePickerHandler {

private final Activity context;
private ValueCallback<Uri[]> filePickerCallback;
private String[] mimeTypes;
private File tempFile;

public FilePickerHandler(Activity context) {
Expand All @@ -41,10 +45,13 @@ void setTempFile(File tempFile) {
}

void openPicker(FileChooserParams fileChooserParams, ValueCallback<Uri[]> 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) {
Expand All @@ -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.
Expand Down Expand Up @@ -97,22 +132,21 @@ private Optional<Uri> 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit c5251e1

Please sign in to comment.