Get Android activity result in Flutter - android

I have a thirdparty Android control that can't be used in Flutter directly. I put it in an Android activity. Then, using information from https://flutter.io/docs/development/platform-integration/platform-channels#step-3b-add-an-android-platform-specific-implementation-using-kotlin, I can successfully launch that activity and perform some actions there. The only part that doesn't work is sending results back from the activity.
Flutter code:
void showDialog() async
{
try {
final Map<String, List<double>> result = await platform.invokeMethod('show_dialog',
<String, String>{
'address': widget.user.address
});
widget.user.address = result.keys.toList()[0];
} on PlatformException catch (e) {
print('Failed to pick address: ${e.message}.');
}
}
Android code:
class MainActivity: FlutterActivity() {
private val CHANNEL = "dialog"
private lateinit var _result: MethodChannel.Result
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "show_dialog") {
_result = result
val intent = Intent(this#MainActivity, DialogActivity::class.java)
intent.putExtra("address", call.argument<String>("address"))
startActivityForResult(intent, 9689)
}
else result.notImplemented()
}
}
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
if(requestCode != 9689)
return super.onActivityResult(requestCode, result, intent)
if (result == Activity.RESULT_OK) {
_result.success(mapOf(intent!!.getStringExtra("address") to
listOf(intent.getDoubleExtra("latitude", 0.0),
intent.getDoubleExtra("longitude", 0.0))))
}
else
_result.success(null)
}
}
What's the problem? Breakpoint on the line widget.user.address = result.keys.toList()[0]; is never reached, suggesting the result is never sent back.

Turns out the code was almost correct. The Android side didn't need any changes, but on the Flutter side I had to make this change:
turn
final Map<String, List<double>> result = await platform.invokeMethod(
into
final result = await platform.invokeMethod(
i. e. simply remove explicit type from the variable, because the return value of platform.invokeMethod was some kind of an internal hash map (in particular, it's name started with an underscore) rather than that of Map as specified. Flutter didn't show any errors in the console output, because for some reason it only captures Android log with debugger attached to the Android part of the application, and once I figured how to debug Android code in Android Studio, I immediately found the reason.

Related

I want my library to start an activity without the main code explictly calling it

I'm working on a library that has a couple of ready-made activities.
So far i have my activities in the library, and in the main app, i call it normally with registerForActivityResult to start it.
this means whoever is using my library would be able to see the whole activity.
what i would like to do, is to have the developer call a method in the library class and ask it to do an action, and in the library that method would on its own start the activity, register it for result, and return the result to the calling class through an interface.
the below is what i tried but it gives me error LifecycleOwner is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED
private fun launchScannerActivity(activity: FragmentActivity, callback: ScannerCallback) {
val scanResult =
activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
callback.onResult(it.data?.getStringExtra("Some Key") ?: "")
} else {
callback.onFail()
}
}
val intent = Intent(activity, ScannerActivity::class.java)
scanResult.launch(intent);
}
why do i need this:
This library would be an SDK for a SAAS product, so we would like to abstract and obfuscate as much of the implementation as possible from our clients.
You can't really communicate between Activities using interfaces, at least not in a way that is somewhat concise and isn't very prone to leaking. What you can do is expose your own Activity result contract. Then your API could be as simple as some of the ones in ActivityResultContracts. You can look at the source code there to see how to implement it.
Maybe something like this:
class ScannerResultContract : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit?): Intent {
return Intent(context, ScannerActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? {
return if (resultCode == Activity.RESULT_OK) {
intent?.getStringExtra("Some Key")
} else {
null
}
}
}
Client usage:
// In activity or fragment:
val getScannerResult = registerForActivityResult(ScannerResultContract()) { resultString ->
if (resultString != null) {
// use it
} else {
// log no result returned
}
}
//elsewhere:
someListener.setOnClickListener {
getScannerResult.launch()
}

How to set request code in Activity Result API?

I'm migrating from startActivityForResult to the Activity Result API. I have custom contract and a launcher:
private val gameContract=object: ActivityResultContract<Int, GameActivity.GameResult?>() {
override fun createIntent(context: Context, input: Int?): Intent {
return Intent(this#SelectorActivity, GameActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): GameActivity.GameResult? {
if (resultCode == Activity.RESULT_OK) {
val data = intent?.getStringExtra("data")
data?.let {
return Gson().fromJson(it, GameActivity.GameResult::class.java)
}
return null
}
return null
}
}
private val gameLauncher=registerForActivityResult(gameContract){
//
}
When I start activity with gameLauncher.launch(0) I got exception
java.lang.IllegalArgumentException: Can only use lower 16 bits for
requestCode
I tried different inputs with the same result
Apparently it expects requestCode to be set. But the API has no means to set it.
I thought the main purpose of this API is to get rid of messy request codes
So how do I start the activity with custom contract?
The problem was is that I forgot to add dependency
implementation 'androidx.fragment:fragment-ktx:1.3.2'

SMS verification code request failed when authenticating using Firebase Auth

When authenticating using Firebase Auth, I want to auto input the code that is received via SMS. I am able to receive SMS and go through auth process manually, but when I use SmsRetriever, the app crashes and then the bottom sheet dialog shows up.
This is everything that that appears in Logcat:
E/FirebaseAuth: [SmsRetrieverHelper] SMS verification code request failed: unknown status code: 17010 null
Code in Fragment where user inputs their phone number:
private val SMS_CONSENT_REQUEST = 2 // Set to an unused request code
private val smsVerificationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
// Get consent intent
val consentIntent = extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
try {
// Start activity to show consent dialog to user, activity must be started in
// 5 minutes, otherwise you'll receive another TIMEOUT intent
startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
} catch (e: ActivityNotFoundException) {
// Handle the exception ...
}
}
CommonStatusCodes.TIMEOUT -> {
// Time out occurred, handle the error.
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val task = SmsRetriever.getClient(requireActivity()).startSmsUserConsent(null)
val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
requireActivity().registerReceiver(smsVerificationReceiver, intentFilter)
}
override fun sendSms() {
showProgressBar(true)
SmsRetriever.getClient(requireActivity()).startSmsUserConsent(presenter.getNumber())
val options = PhoneAuthOptions.newBuilder(auth)
.setPhoneNumber(presenter.getNumber())
.setTimeout(58L, TimeUnit.SECONDS)
.setActivity(requireActivity())
.setCallbacks(callbacks)
.build()
PhoneAuthProvider.verifyPhoneNumber(options)
}
override fun onDestroy() {
super.onDestroy()
requireContext().unregisterReceiver(smsVerificationReceiver)
}
This is the code in Fragment where user has to input the code:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
// ...
SMS_CONSENT_REQUEST ->
// Obtain the phone number from the result
if (resultCode == Activity.RESULT_OK && data != null) {
// Get SMS message content
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
// Extract one-time code from the message and complete verification
// `message` contains the entire text of the SMS message, so you will need
// to parse the string.
message?.let { presenter.parseSms(it) }
// send one time code to the server
} else {
// Consent denied. User can type OTC manually.
}
}
}
Print your FirebaseAuthException error to see what's going on. If you're using a real phone number for development and using it again and again, Firebase might block the device for a time being.
SOLUTION: Add a test phone number with a password and use it.
try to print exception in onFailure like --> {p0.message} print this line logcat and it will definately show --> E/exception in firebase: We have blocked all requests from this device due to unusual activity. Try again later. this is why because we are using this phone number many times for login
It is a too-many-request error screenshot
The solution is to either wait for few hours or -if this user is a test user- , just add the number in test users to fix the code and do not send too many SMSes.

Getting status "Incomplete" on stripe always

I'm always getting incomplete at onCompletePayment and I'm also checked stripe sample app but it's also not working for me. I have check lot but I unable rectify the issue.
So what's would be error on my side?
Source :
PaymentConfiguration.init(BuildConfig.STRIPE_PUBLISHABLE_KEY)
/* Initialized customer*/
private fun setupPaymentSession() {
mPaymentSession = PaymentSession(mvpView?.baseActivity!!)
val paymentSessionInitialized = mPaymentSession!!.init(object : PaymentSession.PaymentSessionListener {
override fun onCommunicatingStateChanged(isCommunicating: Boolean) {
if (isCommunicating) {
} else {
}
}
override fun onError(errorCode: Int, errorMessage: String?) {
}
override fun onPaymentSessionDataChanged(data: PaymentSessionData) {
mPaymentSessionData = data
checkForCustomerUpdates()
}
}, PaymentSessionConfig.Builder()
/* .setPrepopulatedShippingInfo(getExampleShippingInfo())
.setHiddenShippingInfoFields(ShippingInfoWidget.PHONE_FIELD, ShippingInfoWidget.CITY_FIELD)*/
.setShippingInfoRequired(false)
.build())
if (paymentSessionInitialized) {
mPaymentSession?.setCartTotal(20L)
}
}
override fun handlePaymentData(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null) {
mPaymentSession?.handlePaymentData(requestCode, resultCode, data)
mPaymentSession?.completePayment(PaymentCompletionProvider { paymentData, listener ->
Toast.makeText(mvpView?.baseActivity!!, "success" + paymentData.paymentResult, Toast.LENGTH_SHORT).show()
listener.onPaymentResult(paymentData.paymentResult)
})
}
}
Not familiar with Kotlin but in the code snippet you provided, I'd suggest not to override handlePaymentData.
Instead call mPaymentSession.handlePaymentData in the onActivityResult of your host Activity like it is suggested in the doc here or as shown in the example here so that any updates to the PaymentSessionData is reported to your PaymentSessionListener that you attached when initializing PaymentSession (i.e with mPaymentSession!!.init).
Also generally, depending on your app Checkout flow, you would want to call mPaymentSession.completePayment(...) as a result of your user clicking for example on a "Pay" button.
You would pass to the completePayment(...) call a PaymentCompletionProvider which would:
send an HTTP request to your backend so that you can create a charge using Stripe's API
mark the result of the payment using listener.onPaymentResult(...) passing PaymentResultListener.SUCCESS in the case where the payment was for example successful.
I don't think that the example app has an example of this but in Java you could for example have a click listener on your "Pay" button setup like below:
Button payButton = findViewById(R.id.pay_bttn);
payButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mPaymentSessionData.isPaymentReadyToCharge() && mPaymentSessionData.getPaymentResult() == PaymentResultListener.SUCCESS) {
// Use the data to complete your charge - see below.
mPaymentSession.completePayment(new PaymentCompletionProvider() {
#Override
public void completePayment(#NonNull PaymentSessionData data, #NonNull PaymentResultListener listener) {
Log.v(TAG, "Completing payment with data: " + data.toString());
// This is where you want to call your backend...
// In this case mark the payment as Successful
listener.onPaymentResult(PaymentResultListener.SUCCESS);
}
});
}
}
});
I hope this helps.

GoogleSignInClient return 8 (internal error)

I'm trying to connect my game to Google Play Games Services, but when I try to login, it always returns me an error code 8 (internal error).
The code is copy pasted from Google example:
lateinit var signInClient: GoogleSignInClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_settings)
settings_login.setOnClickListener { login() }
signInClient = GoogleSignIn.getClient(this,
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).build()
)
}
private fun login() {
startActivityForResult(signInClient.signInIntent, 9001)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != 9001) {
return
}
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
try {
val account = task.getResult(ApiException::class.java)
onConnected(account)
} catch (apiException: ApiException) {
var message: String? = apiException.message
if (message == null || message.isEmpty()) {
message = getString(R.string.signin_other_error)
}
onDisconnected()
AlertDialog.Builder(this)
.setMessage(message)
.setNeutralButton(android.R.string.ok, null)
.show()
}
}
In Google Play Console I've linked my game with debug keystore SHA-1.
I've checked everythin mentioned in Troubleshooting guide, but I still get this message again and again.
Does someone faced this issue? Any ideas how to debug it?
EDIT:
I found that it actually logs me in - if I restart game, method signInSilently() will be successful. However, it still shows this error 8 when I logout and try to log in manually. Could it be the problem with login activity overlay?
Oh, and I checked api access in Google Play Api Console - it shows that api actually receives my calls and it doesn't mention any errors.
EDIT 2: I've added requestEmail() to GoogleSignInOptions.Builder, and it shows me overlay with access request. However, it still fails in GoogleSignIn.getSignedInAccountFromIntent(intent).getResult(ApiException::class.java) with same error (8 - internal error).
It looks like this bug in Google Play Services 12.2.21:
https://github.com/googlesamples/google-services/issues/358
Google is supposed to be working on a fix for release over the air soon..
it's maybe late but I found the reason. It fixed in my case and I see your code has same problem.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
...
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
...
}
The intent you passed to the method getSignedInAccountFromIntent() is not the intent that returned by onActivityResult. The intent you passed come from activity, so you need to change it to
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
"data" is the intent returned by onActivityResult
I didn't found the reason of this error, but I found an (ugly) workaround. I noticed, that when I restart game after manual login, even if there was this error, signInSilently() method works fine, which means that API actually authenticate me and fails later. So in catch block I'm checking for status code of error, and, if it's (8 - internal error), I'm requesting last signed in account. If account is present, I assume user to be logged in.
It's really dirty but I'm out of ideas.
//onActivityResult
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
try {
val account = task.getResult(ApiException::class.java)
onSuccess(account)
} catch (apiException: ApiException) {
val acc = GoogleSignIn.getLastSignedInAccount(context)
if (apiException.statusCode == 8 && acc != null && acc.email != null) {
onSuccess(account)
} else {
onFail(apiException)
}
}
I have a client with that error. Only ONE! With a Galaxy S9. Nothing happens when clicking on the Sign In button (startActivityForResult -> GoogleSignIn.getClient.getSignInIntent)

Categories

Resources