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'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.
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.
I am using the IabHelper utility classes, as recommended by Google's tutorial, and I'm being hit hard by this error. Apparently IabHelper can not run multiple async operations at the same time. I even managed to hit it by trying to start a purchase while the inventory taking was still in progress.
I have already tried to implement onActivityResult in my main class as suggested here, but I don't even get a call to that method before the error hits. Then I found this but I have no idea where to find this flagEndAsync method - it's not in the IabHelper class.
Now I'm looking for a way around this (without reimplementing the whole she-bang). The only solution I can think of is to create a boolean field asyncActive that is checked before an async task is started, and not do it if there is another task active. But that has many other problems, and doesn't work across activities. Also I'd prefer to have an async task queue up and run as soon as it's allowed to, instead of not running at all.
Any solutions for this issue?
A simple tricky solution
before calling purchaseItem method just add this line
if (billingHelper != null) billingHelper.flagEndAsync();
so your code looks this way
if (billingHelper != null) billingHelper.flagEndAsync();
purchaseItem("android.test.purchased");
Note: don't forget to make public flagEndAsync() method in IabHelper if you call it from another package.
Make sure that you call the IabHelper's handleActivityResult in the Activity's onActivityResult, and NOT in the Fragment's onActivityResult.
The following code snippet is from TrivialDrive's MainActivity:
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);
if (mHelper == null) return;
// Pass on the activity result to the helper for handling
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
// not handled, so handle it ourselves (here's where you'd
// perform any handling of activity results not related to in-app
// billing...
super.onActivityResult(requestCode, resultCode, data);
}
else {
Log.d(TAG, "onActivityResult handled by IABUtil.");
}
}
Update:
There is now a In-app Billing Version 3 API (what was the version in 2013?)
The code sample has moved to Github. Snippet above edited to reflect current sample, but is logically the same as before.
This was not easy to crack but I found the needed workarounds. Quite disappointed with Google lately, their Android web sites have become a mess (very hard to find useful info) and their sample code is poor. When I was doing some Android development a few years ago it all went so much easier! This is yet another example of that...
Indeed IabUtil is buggy, it does not properly call off its own async tasks. The complete set of necessary workarounds to stabilise this thing:
1) make method flagEndAsync public. It is there, just not visible.
2) have every listener call iabHelper.flagEndAsync to make sure the procedure is marked finished properly; it seems to be needed in all listeners.
3) surround calls with a try/catch to catch the IllegalStateException which may occur, and handle it that way.
I ended up doing something similar to Kintaro. But added mHelper.flagEndAsync() to the end of the catch. The user still gets the toast but by the next time they push the purchase button, the async operation has been killed and the purchase button is ready to go again.
if (mHelper != null) {
try {
mHelper.launchPurchaseFlow(this, item, RC_REQUEST, mPurchaseFinishedListener, "");
}
catch(IllegalStateException ex){
Toast.makeText(this, "Please retry in a few seconds.", Toast.LENGTH_SHORT).show();
mHelper.flagEndAsync();
}
}
Find flagEndAsync() inside IabHelper.java file and change it to a public function.
Before trying purchase call flagEndAsync() for your IabHelper
You must do somthig like this code :
mHelper.flagEndAsync();
mHelper.launchPurchaseFlow(AboutActivity.this, SKU_PREMIUM, RC_REQUEST, mPurchaseFinishedListener, "payload-string");
I was having the same issue until I stumbled upon another SO thread. I'm including a touched up version of the code found in the other thread that you need to include in your Activity that initialises the purchase.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Pass on the activity result to the helper for handling
// NOTE: handleActivityResult() will update the state of the helper,
// allowing you to make further calls without having it exception on you
if (billingHelper.handleActivityResult(requestCode, resultCode, data)) {
Log.d(TAG, "onActivityResult handled by IABUtil.");
handlePurchaseResult(requestCode, resultCode, data);
return;
}
// What you would normally do
// ...
}
A simple trick that did it for me was to create a method in IabHelper:
public Boolean getAsyncInProgress() {
return mAsyncInProgress;
}
and then in your code, just check:
if (!mHelper.getAsyncInProgress())
//launch purchase
else
Log.d(TAG, "Async in progress already..)
Really annoying issue. Here is a quick and dirty solution that is not perfect code wise, but that is user friendly and avoids bad ratings and crashes:
if (mHelper != null) {
try {
mHelper.launchPurchaseFlow(this, item, RC_REQUEST, mPurchaseFinishedListener, "");
}
catch(IllegalStateException ex){
Toast.makeText(this, "Please retry in a few seconds.", Toast.LENGTH_SHORT).show();
}
}
This way the user just has to tap another time (2 times at worst) and gets the billing popup
Hope it helps
Just check for the onActivityResult requestCode on the activity and if it matches the PURCHASE_REQUEST_CODE you used on the purchase just pass it to the fragment.
When you add or replace the fragment in the FragmentTransaction just set a tag:
fTransaction.replace(R.id.content_fragment, fragment, fragment.getClass().getName());
Then on your activity's onActivityResult
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == PurchaseFragment.PURCHASE_REQUEST_CODE) {
PurchaseFragment fragment = getSuportFragmentManager().findFragmentByTag(PurchaseFragment.class.getNAme());
if(fragment != null) {
fragment.onActivityResult(requestCode, resultCode, data);
}
}
}
if you code in fragment then you this code in IabHelper.java
void flagStartAsync(String operation) {
if (mAsyncInProgress) {
flagEndAsync();
}
if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
mAsyncOperation = operation;
mAsyncInProgress = true;
logDebug("Starting async operation: " + operation);
}
Or, you can get the latest IabHelper.java file here: https://code.google.com/p/marketbilling/source/browse/
The March 15th version fixed this for me. (Note other files with no changes were committed on the 15th)
I still had to fix one crash that happened during testing caused by a null intent extras when "android.test.canceled" was the sent SKU. I changed:
int getResponseCodeFromIntent(Intent i) {
Object o = i.getExtras().get(RESPONSE_CODE);
to:
int getResponseCodeFromIntent(Intent i) {
Object o = i.getExtras() != null ? i.getExtras().get(RESPONSE_CODE) : null;
I have had this issue occasionally, and in my case I've tracked it down to the fact that if the onServiceConnected method in IabHelper can be called more than once if the underlying service disconnects and reconnects (e.g. due to an intermittent network connection).
The specific operations in my case were "Can't start async operation (refresh inventory) because another async operation(launchPurchaseFlow) is in progress."
The way that my app is written, I can't call launchPurchaseFlow until after I've had a completed queryInventory, and I only call queryInventory from my onIabSetupFinished handler function.
The IabHelper code will call this handler function whenever its onServiceConnected is called, which can happen more than once.
The Android documentation for onServiceDisconnected says:
Called when a connection to the Service has been lost. This typically happens when the
process hosting the service has crashed or been killed. This does not remove the
ServiceConnection itself -- this binding to the service will remain active, and you will
receive a call to onServiceConnected(ComponentName, IBinder) when the Service is next
running.
which explains the problem.
Arguably, IabHelper shouldn't call the onIabSetupFinished listener function more than once, but on the other hand it was trivial to fix the problem in my app by simply not calling queryInventory from within this function if I've already done it and got the results.
Another major issue with the IabHelpr class is the poor choice of throwing RuntimeExcptions (IllegalStateException) in multiple methods. Throwing RuntimeExeptions from your own code in most cases is not desirable due to the fact that they are unchecked exceptions. That is like sabotaging your own application- if not caught, these exceptions will bubble up and crash your app.
The solution to this is to implement your own checked exception and change the IabHelper class to throw it, instead of the IllegalStateException. That will force you to handle this exception everywhere it could be thrown in your code at compile time.
Here is my custom exception:
public class MyIllegalStateException extends Exception {
private static final long serialVersionUID = 1L;
//Parameterless Constructor
public MyIllegalStateException() {}
//Constructor that accepts a message
public MyIllegalStateException(String message)
{
super(message);
}
}
Once we make the changes in the IabHelper class, we can handle our checked exception in our code where we call the class methods. For example:
try {
setUpBilling(targetActivityInstance.allData.getAll());
} catch (MyIllegalStateException ex) {
ex.printStackTrace();
}
I had the same issue and the problem was that I didn't implement the method onActivityResult.
#Override
protected void onActivityResult(int requestCode,
int resultCode,
Intent data)
{
try
{
if (billingHelper == null)
{
return;
} else if (!billingHelper.handleActivityResult(requestCode, resultCode, data))
{
super.onActivityResult(requestCode, resultCode, data);
}
} catch (Exception exception)
{
super.onActivityResult(requestCode, resultCode, data);
}
}
Yes, i am also facing this issue but i resolved this but i resolved using
IabHelper mHelpermHelper = new IabHelper(inappActivity, base64EncodedPublicKey);
mHelper.flagEndAsync();
The above method stop all the flags. Its work for me must check
This answer directly addresses the problem that #Wouter has seen...
It is true that onActivityResult() must be triggered, like many people have said. However, the bug is that Google's code isn't triggering onActivityResult() in certain circumstances, i.e. when you're pressing your [BUY] button twice when running the debug build of your app.
Additionally, one major problem is that the user may be in a shaky environment (i.e. Bus or subway) and presses your [BUY] button twice... suddenly you've got yourself an exception !
At least Google fixed this embarrassing exception https://github.com/googlesamples/android-play-billing/commit/07b085b32a62c7981e5f3581fd743e30b9adb4ed#diff-b43848e47f8a93bca77e5ce95b1c2d66
Below is what I implemented in the same class where IabHelper is instantiated (for me, this is in the Application class) :
/**
* invokes the startIntentSenderForResult - which will call your activity's onActivityResult() when it's finished
* NOTE: you need to override onActivityResult() in your activity.
* NOTE2: check IAB code updates at https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main/java/com/example/android/trivialdrivesample/util
* #param activity
* #param sku
*/
protected boolean launchPurchaseWorkflow(Activity activity, String sku)
{
if (mIabIsInitialized)
{
try
{
mHelper.launchPurchaseFlow(
activity,
sku,
Constants.PURCHASE_REQUEST_ID++,// just needs to be a positive number and unique
mPurchaseFinishedListener,
Constants.DEVELOPER_PAYLOAD);
return true;//success
}
catch (IllegalStateException e)
{
mHelper.flagEndAsync();
return launchPurchaseWorkflow(activity, sku);//recursive call
}
}
else
{
return false;//failure - not initialized
}
}
My [BUY] button calls this launchPurchaseWorkflow() and passes the SKU and the activity the button is in (or if you're in a fragment, the enclosing activity)
NOTE: be sure to make IabHelper.flagEndAsync() public.
Hopefully, Google will improve this code in the near future; this problem is about 3 years old and it's still an ongoing problem :(
My solution is simple
1.) Make the mAsyncInProgress variable visible outside of IabHelper
public boolean isAsyncInProgress() {
return mAsyncInProgress;
}
2.) Use this in your Activity like:
...
if (mIabHelper.AsyncInProgress()) return;
mIabHelper.queryInventoryAsync(...);
...
A little-modified version of NadtheVlad's answer that works like charm
private void makePurchase() {
if (mHelper != null) {
try {
mHelper.launchPurchaseFlow(getActivity(), ITEM_SKU, 10001, mPurchaseFinishedListener, "");
}
catch(IllegalStateException ex){
mHelper.flagEndAsync();
makePurchase();
}
}
}
The logic is simple, just put the launchPurchaseFlow() thing in a method and use recursion in the catch block. You still need to make flagEndAsync() public from the IabHelper class.
I have same issue, but it resolved!
I think you must be not run "launchPurchaseFlow" on UI thread, try to run launchPurchaseFlow on UI thread,
it would be working fine!
mActivity.runOnUiThread(new Runnable(){
public void run(){
mHelper.launchPurchaseFlow(mActivity, item, 10001, mPurchaseFinishedListener,username);
}
});