I'm developing a custom compound View that needs to access external storage. How can I implement the permission handling without involving outside parties, i.e. Activity or Fragment?
I get that I can request the permissions using the View's context, but how can I handle onRequestPermissionsResult() inside the View? Is it even possible?
If it's not possible, what would be the most elegant solution to handle something like this?
I'm developing a custom compound View that needs to access external storage
IMHO, that's an architecture bug. A View is for displaying stuff to the user, and sometimes for collecting low-level input events and turning them into higher-order constructs (e.g., clicks, swipes). A View should not have any connection to files, databases, etc. See the MVC, MVP, MVVM, and similar GUI architecture patterns.
WebView, which does not abide by this, causes problems (e.g., doing disk I/O on the main application thread) as a result.
How can I implement the permission handling without involving outside parties, i.e. Activity or Fragment?
You can't. It is the responsibility of the activity or fragment to request the permission, presumably before your view needs this data.
what would be the most elegant solution to handle something like this?
Extract the data-access portion of this View into something else that is managed by the activity or fragment, where the threading, permissions, and other work associated with that data access can be managed.
You can't work with permissions without the instance of the activity, but you can do your code prettier. If you want to send a request and handle it in one place, then you can use the example below.
Just create something looks like BaseActivity and put there such code
public class PermActivity extends Activity {
interface OnPermissionCallback{
void requestResult(String[] permissions, int[] grantResults);
}
private SparseArray<OnPermissionCallback> permissionCallback = new SparseArray<>();
#Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
permissionCallback.get(requestCode).requestResult(permissions, grantResults);
}
public void addPermissionCallback(int requestCode, OnPermissionCallback callback){
permissionCallback.put(requestCode, callback);
}
}
And now in our client code, we can do something like that
class SomeClasThatWorksWithPerms{
private PermActivity activity;
public SomeClasWorksWithPerms(PermActivity activity) {
this.activity = activity;
}
void foo(){
if (ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
// do something
}else {
activity.addPermissionCallback(0, (perms, grantResults) -> {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
foo(); // try one more
}
});
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 0);
}
}
}
I have used spareArray and indexation by the request code but you can use another way of storing callbacks.
It's very simple example, you can see something more serious there
https://github.com/mrizyver/Fl_Launcher/blob/master/app/src/main/java/com/izyver/fllauncher/presentation/activity/FlActivity.kt - as you can see, it is activity
https://github.com/mrizyver/Fl_Launcher/blob/master/app/src/main/java/com/izyver/fllauncher/presentation/loaders/WallpaperLoader.kt - our client code that works with permissions
let us assume you need to call the requestPermissionLauncher from a dialog fragment when a user clicks on "OK" or some other button. here is the requestPermissionLauncher found in MainActivity or you can put it in any other activity where the dialog fragment is called from.
public ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// features requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
});
here is the code source if you want to refer https://developer.android.com/training/permissions/requesting
Then in your dialog fragment use the following code to call to the instance requestPermissionLauncher
((MainActivity)getContext()).requestPermissionLauncher.launch(Manifest.permission.[*your permission goes here*]);
It's only possible in Activities and Fragments.
What you can do is copy public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) in your View and call that method in the corresponding one in the Activity or Fragment where the Context is.
Related
I'm currently using the latest version of Xamarin.Forms (4.5.0.617) and Xamarin.Essentials (1.5.2).
I have a dependency injected service which is responsible for getting access to phone contacts, it's effectively a wrapper around the Xamarin.Essentials permissions code. I need to guarantee that the code is executed on the UI thread to avoid exceptions. I've got as far as the below code, however, it isn't working as i'd expect. In my app, the permissions popup appears and offers me the choice allow/deny but then all code after RequestAsync fails to execute like a response is never officially returned. The next time I run the app, it works straight away (so presumably permission in the background has been correctly recorded).
public async Task<bool> RequestAccessPhoneContacts()
{
PermissionStatus status = PermissionStatus.Denied;
status = await MainThread.InvokeOnMainThreadAsync<PermissionStatus>(async () =>
{
return await Permissions.RequestAsync<Permissions.ContactsRead>();
});
return (status == PermissionStatus.Granted);
}
I'm not sure if i've caused an issue with the way i'm trying to force the code onto the UI thread or whether i'm using the async code incorrectly... or (least likely) whether it's a bug in the Xamarin.Essentials Permissions code. I've only seen this behaviour on Android at the moment, but I haven't tested it on any others.
Any help greatly appreciated :)
thanks!
I had a similar issue when I called current location using Xamarin.Essentials. I got this exception:
Xamarin.Essentials.PermissionException: 'Permission request must be invoked on main thread.'
& this helped me resolve.
Call current location from main thread
private Task<Location> GetLastKnownLocation()
{
var locationTaskCompletionSource = new TaskCompletionSource<Location>();
Device.BeginInvokeOnMainThread(async () =>
{
locationTaskCompletionSource.SetResult(await Geolocation.GetLastKnownLocationAsync());
});
return locationTaskCompletionSource.Task;
}
And call above method inside Try catch block & also use configure await false
private async Task ExecuteGetGeoLocationCommand()
{
try
{
var location = await GetLastKnownLocation().ConfigureAwait(false);
}
catch (FeatureNotSupportedException fnsEx) {}
catch (Exception ex){}
}
Turns out I hadn't followed the documentation properly when setting this up.
After adding this into my main activity, everything kicks back to life.
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
This information came out of me raising a bug on the Xamarin.Essentials github -
https://github.com/xamarin/Essentials/issues/1227
I am currently working on a library project for Android which I plan on open sourcing. The library has an activity that I need to return a result for so the app that's utilising the library will need to call startActivityForResult.
My question is, if the user has an activity within their app, which calls a second activity within their own app which also needs to return a result, and this activity needs to call the activity from my library, my libraries activity, and their own activity will be using the same onActivityResult callback. Is there a way to avoid my activities request code, not conflicting with one of their own request codes, is it just a case of assume their own request codes are 1, 2, 3 etc and I start my libraries activity request code from some arbitrary number like 1000.
Is this just the way it works or is there a better way to avoid my request code for my library conflicting with another apps activities request code?
I think the library should give a parameter to specify request code by developer themselves for startActivityForResult, so they could never be conflicted in the same activity or fragment they have been called from.
Activity A can choose its own request codes, and Activity B will never know which request code is used by A.
Which is no problem because request codes are purely local. Each Activity instance is separate from other Activity instances - they won't be mixed up just because they all implement the same method (like onCreate() or in your case onActivityResult() ).
Let's take a look at some lines from the source code for android.app.Activity, starting at line 4614
public void startActivityForResult(#RequiresPermission Intent intent, int requestCode,
#Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
// If this start is requesting a result, we can avoid making
// the activity visible until the result is received. Setting
// this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
// activity hidden during this time, to avoid flickering.
// This can only be done when a result is requested because
// that guarantees we will get information back when the
// activity is finished, no matter what happens to it.
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
The comments in the quoted code snippet show that the request code is used to determine if there is a result to be returned.
Please note that the parameters for startActivityForResult(Intent intent, int requestCode, Bundle options) are passed into a method execStartActivity() from the Instrumentation class in the same package android.app.
But there are four other parameters to execStartActivity() which serve to identify the calling app and the current Activity instance (I won't go into details of the Binder framework here, but there is for example a youtube video on this topic):
Context who, IBinder contextThread, IBinder token, Activity target
Again, the request code is only used to determine if there is a result to be returned (for a negative request code, startActivityForResult() is handled just like startActivity())
return requestCode >= 0 ? am.getResult() : null;
Besides that, the request code is just passed back to the Activity which called startActivityForResult().
So if an Activity doesn't use the same request code for different types of requests, all is good.
I have a VideoCallPageRenderer like below
VideoCallPageRenderer : PageRenderer, Android.Support.V4.App.ActivityCompat.IOnRequestPermissionsResultCallback
{
public const int REQUEST_MIC = 0;
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
// Other codes
RequestMicPermission();
}
public void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
{
if (requestCode == REQUEST_MIC)
{
if (grantResults.Length == 1 && grantResults[0] == Permission.Granted)
{
}
}
}
private void RequestMicPermission()
{
Android.Support.V4.App.ActivityCompat.RequestPermissions((Activity)Xamarin.Forms.Forms.Context, new string[] { Android.Manifest.Permission.RecordAudio }, REQUEST_MIC);
}
}
Here RequestMicPermission works fine because I can see the pop up coming on screen asking for permission. But after I allow or deny OnRequestPermissionsResult is not called.
Any help? It would be very hard to try to override it in the Activity.
For anyone else having an issue here, I was not seeing the OnRequestPermissionsResult being called either. But I had the debugger attached, the handler wasn't doing anything, and even though the breakpoint was valid, it was never hit.
I finally did a clean and rebuild and voila, the breakpoint was hit.
Just in case someone else was tearing their hair out like myself :).
For future readers that encountered this problem too, a good approach is abstract the permissions features in a service and register it with Xamarin FormĀ“s Dependencyservice.
You can resolve this service in your renderer, or any non-activity class, to use it. Also resolve it in MainActivity, override OnRequestPermissionsResult, and call yourService.OnPermissionResult to communicate the result.
As Koushik says ActivityCompat.IOnRequestPermissionsResultCallback doesnt work because we call RequestPermissions with reference to MainActivity, so the result is obtained in this activity.
You can see an implementation example in my Github:
GpsService
MainActivity
I am aware about the changes introduced in Android 6.0/SDKVersion 23 regarding the run-time permission. There is already discussion and talks on this topic in the below post which talks about the various aspects of new permission model.
"How to request permissions from a Service in Android Marshmallow"
Google I/O 2015 - Android M Permissions
"Mother, May I?" Asking for Permissions (Android Dev Summit 2015)
After going thorough these article,I believe below is suggested to "workaround" (as Service doesnot have UI support)this problem.
Check permission checkSelfPermission() in the context of
Service.
Notify it to status bar in case the permission is denied.
User would now click on status bar which would launch a new
DialogActivity to request for permission when users press on the
notification.
I am not able to make the workable version of the suggestions provided to achieve this task. When we launch DialogActivity to ask for permission, it would be assigned and available for that activity. It would not be applicable for background service who had put it on status bar and even after this service would not be granted this permission(instead the permission would be given to DialogActivity).
Could somebody provide the input(possibly workable logic/code) so that I can make it workable(.i.e. execute the code in the context of Service which would be dependent on whether permission is granted or not).
My Scenario
Develop application which would send the SMS at regular interval
regarding the my current location.
I have designed the UI for this application as mentioned below. It had some settings parameters and also buttons which would start the main logic which is:
Start monitoring the current location and
Send the SMS to configured contact numbers.
I believe user would minimize this application( as it does not have any interesting logic apart from sending the SMS) and it should continue to work in background(.i.e. monitoring the location and sending the SMS). Hence I thought to use move this logic in Service instead of keeping it the Activity itself. If I keep these logic in Activity itself, it works as expected however for that user is suppose to keep the application in foreground all the time.
So far I have been able to achieve the following:
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Define START/STOP button handles.
mStartApp = (ImageButton)findViewById(R.id.startApp);
mStopApp = (ImageButton)findViewById(R.id.stopApp);
//Write event handlers for above grabbed buttons.
mStartApp.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent startService = new Intent(getApplicationContext(),
CurrentLocationTrackerService.class);
startService(startService);
}
});
mStopApp.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent stopService = new Intent(getApplicationContext(),
CurrentLocationTrackerService.class);
stopService(stopService);
}
});
// Support for the ActionBar widgets menu in current activity.
Toolbar mToolBar = (Toolbar)findViewById(R.id.myToolbar);
if(mToolBar != null) {
setSupportActionBar(mToolBar);
}
}
//Other methods.....
}
public class CurrentLocationTrackerService extends Service {
public CurrentLocationTrackerService() { }
#Override
public void onCreate() { }
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
int permissionLocationAccess = ContextCompat.checkSelfPermission(getApplicationContext(),
Manifest.permission.ACCESS_FINE_LOCATION);
// If permissionLocationAccess is true execute further
// If permissionLocationAccess false(which would always as we are in Service and it is dangerous
// permission) we need to put some information to status bar. Post that user would click on that
// which would launch some new activity where UI of asking permission would be shown. But still
// Service would not get the permission( only new activity would have that permission but it is
// of no use here as I am planning to put these logic iside the Service as my app may not be always
// be in foreground)
}
}
Hope I have provided all detail regarding the problem and also my application context regarding why I require to model in this way.The real point over here is how to achieve this.
I am really stuck at this point and any help would be highly appreciated. Kindly let me know in case anything else is required from my side.
Because the Android SDK 23 gives users the possibility to deny apps access to certain functionalities I wanted to update one of my apps to request permissions as it is described in here: https://developer.android.com/preview/features/runtime-permissions.html.
In one of the activities I embed a SupportMapFragment. To make it work you need to have the WRITE_EXTERNAL_STORAGE permission, so I request it when I start the activity which results in a creation of a permission request dialog.
Now the problem is that when the dialog is still open and I rotate the device the activity will be restarted and open a new permission request dialog while the old one is still there. The result is two of those dialogs on top of each other and only one of it being useful.
Is there a way to get rid of the dialog that was started first?
As CommonsWare said in his comment the best solution is to put a boolean into the savedInstanceState-Bundle to know if the dialog is still open.
Example:
// true if dialog already open
private boolean alreadyAskedForStoragePermission = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null) {
alreadyAskedForStoragePermission = savedInstanceState.getBoolean(STORAGE_PERMISSION_DIALOG_OPEN_KEY, false);
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY, alreadyAskedForStoragePermission);
}
private void checkStoragePermission(){
if(alreadyAskedForStoragePermission){
// don't check again because the dialog is still open
return;
}
if(ActivityCompat.checkSelfPermission(this, STORAGE_PERMISSIONS[0]) != PackageManager.PERMISSION_GRANTED){
// the dialog will be opened so we have to keep that in memory
alreadyAskedForStoragePermission = true;
ActivityCompat.requestPermissions(this, STORAGE_PERMISSIONS, STORAGE_PERMISSION_REQUEST_CODE);
} else {
onStoragePermissionGranted();
}
}
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
switch (requestCode){
case STORAGE_PERMISSION_REQUEST_CODE:
// the request returned a result so the dialog is closed
alreadyAskedForStoragePermission = false;
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
onStoragePermissionGranted();
}
break;
}
}
As #user1991776 mentioned there is actually an undocumented extra that contains whether or not there is a permission dialog open at the moment, in Activity:
private static final String HAS_CURENT_PERMISSIONS_REQUEST_KEY =
"android:hasCurrentPermissionsRequest";
However there is a better way. When you request a permission dialog the second time (due to a rotation), Activity automatically cancels the old dialog by calling your onRequestPermissionResult() with empty arrays:
public final void requestPermissions(#NonNull String[] permissions, int requestCode) {
if (mHasCurrentPermissionsRequest) {
Log.w(TAG, "Can reqeust only one set of permissions at a time");
// Dispatch the callback with empty arrays which means a cancellation.
onRequestPermissionsResult(requestCode, new String[0], new int[0]);
return;
}
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
mHasCurrentPermissionsRequest = true;
}
Or course this behaviour isn't documented because this is Android, and who wants to document complex behaviour?
Anyway you can just always request permissions in onCreate() and then ignore calls to onRequestPermissionsResult() with zero-length permissions arrays.
I guess as this is a system dialog you cannot control it. You could instead prevent that your activity gets reloaded if you turn your device.