Google Fit API does not work in internal testing - android

I am using Google Fit API to get fitness data for a kotlin app.
But API does not work in internal testing on Google Play Developer Console.
When connecting the USB cable and install the APK directly, it will succeed, so I think that the Google API setting(Sha1 Finger print,etc) is correct.
For the part that links with the API, the official github code is used as it is.
Do you have any information about my error?
/**
* This enum is used to define actions that can be performed after a successful sign in to Fit.
* One of these values is passed to the Fit sign-in, and returned in a successful callback, allowing
* subsequent execution of the desired action.
*/
enum class FitActionRequestCode {
SUBSCRIBE,
READ_DATA
}
/**
* This sample demonstrates combining the Recording API and History API of the Google Fit platform
* to record steps, and display the daily current step count. It also demonstrates how to
* authenticate a user with Google Play Services.
*/
class MainActivity : AppCompatActivity() {
private val fitnessOptions = FitnessOptions.builder()
.addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE)
.addDataType(DataType.TYPE_STEP_COUNT_DELTA)
.build()
private val runningQOrLater =
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// This method sets up our custom logger, which will print all log messages to the device
// screen, as well as to adb logcat.
initializeLogging()
checkPermissionsAndRun(FitActionRequestCode.SUBSCRIBE)
}
private fun checkPermissionsAndRun(fitActionRequestCode: FitActionRequestCode) {
if (permissionApproved()) {
fitSignIn(fitActionRequestCode)
} else {
requestRuntimePermissions(fitActionRequestCode)
}
}
/**
* Checks that the user is signed in, and if so, executes the specified function. If the user is
* not signed in, initiates the sign in flow, specifying the post-sign in function to execute.
*
* #param requestCode The request code corresponding to the action to perform after sign in.
*/
private fun fitSignIn(requestCode: FitActionRequestCode) {
if (oAuthPermissionsApproved()) {
performActionForRequestCode(requestCode)
} else {
requestCode.let {
GoogleSignIn.requestPermissions(
this,
requestCode.ordinal,
getGoogleAccount(), fitnessOptions
)
}
}
}
/**
* Runs the desired method, based on the specified request code. The request code is typically
* passed to the Fit sign-in flow, and returned with the success callback. This allows the
* caller to specify which method, post-sign-in, should be called.
*
* #param requestCode The code corresponding to the action to perform.
*/
private fun performActionForRequestCode(requestCode: FitActionRequestCode) = when (requestCode) {
FitActionRequestCode.READ_DATA -> readData()
FitActionRequestCode.SUBSCRIBE -> subscribe()
}
var gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.build()
private fun oAuthPermissionsApproved() = GoogleSignIn.hasPermissions(
getGoogleAccount(),
fitnessOptions
)
/**
* Gets a Google account for use in creating the Fitness client. This is achieved by either
* using the last signed-in account, or if necessary, prompting the user to sign in.
* `getAccountForExtension` is recommended over `getLastSignedInAccount` as the latter can
* return `null` if there has been no sign in before.
*/
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(this, fitnessOptions)
/**
* Handles the callback from the OAuth sign in flow, executing the post sign in function
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (resultCode) {
RESULT_OK -> {
val postSignInAction = FitActionRequestCode.values()[requestCode]
postSignInAction.let {
performActionForRequestCode(postSignInAction)
}
}
else -> oAuthErrorMsg(requestCode, resultCode)
}
}
private fun oAuthErrorMsg(requestCode: Int, resultCode: Int) {
val message = """
There was an error signing into Fit. Check the troubleshooting section of the README
for potential issues.
Request code was: $requestCode
Result code was: $resultCode
""".trimIndent()
Log.e(TAG, message)
}
/** Records step data by requesting a subscription to background step data. */
private fun subscribe() {
// To create a subscription, invoke the Recording API. As soon as the subscription is
// active, fitness data will start recording.
Fitness.getRecordingClient(this, getGoogleAccount())
.subscribe(DataType.TYPE_STEP_COUNT_CUMULATIVE)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.i(TAG, "Successfully subscribed!")
} else {
Log.w(TAG, "There was a problem subscribing.", task.exception)
}
}
}
/**
* Reads the current daily step total, computed from midnight of the current day on the device's
* current timezone.
*/
private fun readData() {
Fitness.getHistoryClient(this, getGoogleAccount())
.readDailyTotal(DataType.TYPE_STEP_COUNT_DELTA)
.addOnSuccessListener { dataSet ->
val total = when {
dataSet.isEmpty -> 0
else -> dataSet.dataPoints.first().getValue(Field.FIELD_STEPS).asInt()
}
Log.i(TAG, "Total steps: $total")
}
.addOnFailureListener { e ->
Log.w(TAG, "There was a problem getting the step count.", e)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the main; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.action_read_data) {
fitSignIn(FitActionRequestCode.READ_DATA)
return true
}
return super.onOptionsItemSelected(item)
}
/** Initializes a custom log class that outputs both to in-app targets and logcat. */
private fun initializeLogging() {
// Wraps Android's native log framework.
val logWrapper = LogWrapper()
// Using Log, front-end to the logging chain, emulates android.util.log method signatures.
Log.setLogNode(logWrapper)
// Filter strips out everything except the message text.
val msgFilter = MessageOnlyLogFilter()
logWrapper.next = msgFilter
// On screen logging via a customized TextView.
val logView = findViewById<View>(R.id.sample_logview) as LogView
TextViewCompat.setTextAppearance(logView, R.style.Log)
logView.setBackgroundColor(Color.WHITE)
msgFilter.next = logView
Log.i(TAG, "Ready")
}
private fun permissionApproved(): Boolean {
val approved = if (runningQOrLater) {
PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACTIVITY_RECOGNITION
)
} else {
true
}
return approved
}
private fun requestRuntimePermissions(requestCode: FitActionRequestCode) {
val shouldProvideRationale =
ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.ACTIVITY_RECOGNITION
)
// Provide an additional rationale to the user. This would happen if the user denied the
// request previously, but didn't check the "Don't ask again" checkbox.
requestCode.let {
if (shouldProvideRationale) {
Log.i(TAG, "Displaying permission rationale to provide additional context.")
Snackbar.make(
findViewById(R.id.main_activity_view),
R.string.permission_rationale,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.ok) {
// Request permission
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACTIVITY_RECOGNITION),
requestCode.ordinal
)
}
.show()
} else {
Log.i(TAG, "Requesting permission")
// Request permission. It's possible this can be auto answered if device policy
// sets the permission in a given state or the user denied the permission
// previously and checked "Never ask again".
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACTIVITY_RECOGNITION),
requestCode.ordinal
)
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>,
grantResults: IntArray
) {
when {
grantResults.isEmpty() -> {
// If user interaction was interrupted, the permission request
// is cancelled and you receive empty arrays.
Log.i(TAG, "User interaction was cancelled.")
}
grantResults[0] == PackageManager.PERMISSION_GRANTED -> {
// Permission was granted.
val fitActionRequestCode = FitActionRequestCode.values()[requestCode]
fitActionRequestCode.let {
fitSignIn(fitActionRequestCode)
}
}
else -> {
// Permission denied.
// In this Activity we've chosen to notify the user that they
// have rejected a core permission for the app since it makes the Activity useless.
// We're communicating this message in a Snackbar since this is a sample app, but
// core permissions would typically be best requested during a welcome-screen flow.
// Additionally, it is important to remember that a permission might have been
// rejected without asking the user for permission (device policy or "Never ask
// again" prompts). Therefore, a user interface affordance is typically implemented
// when permissions are denied. Otherwise, your app could appear unresponsive to
// touches or interactions which have required permissions.
}
}
}
}```
[1]: https://i.stack.imgur.com/UpQLO.png

We had the exact same issue and it doesn't seem to be documented anywhere. Even tho this question is over 1-year old I think it's nice to have it answered.
When you upload an APK for internal testing, it's not signed with your certificate, it's signed using a certificate issued by Google. For this reason, the SHA1 fingerprint configured for the fitness API does not match.
To overcome the issue, what you have to do is go to the developer console> internal testing > version details > downloads and download the apk from there. Once you have the apk, obtain the sha-1 fingerprint directly from it using
keytool -printcert -jarfile fileName.apk
That'll print the details of the certificate, including the SHA-1 fingerprint. Configure that fingerprint for the fitness oauth certificate and it'll work!
Remember to change this to the correct fingerprint when going to beta/production
It's nice to provide a closeup for this

Related

BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS but KeyPairGenerator.initialize() => java.lang.IllegalStateException on API 29

Sorry for such a long question. I've tried to include all relevant information and it is quite a lot. I've been working on this issue for a few weeks now and am in desperate need of help.
General info
I am developing a flutter app that requires authentication with a CryptoObject for certain functionality. This means for Android that setUserAuthenticationRequired(true) needs to be set on the KeyGenParameterSpec that's used to create the key. On Android API >=30 this works fine and I can authenticate myself with either fingerprint or Device Credentials (PIN, pattern, password).
The problem
The problem is that I can't get Biometrics with setUserAuthenticationRequired(true) to work on emulators with API 29, even if they have fingerprints set up. I have not been able to test on emulators with an even lower API, so I don't know if that would work or not.
Calling BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS as below returns true. The else case is run since Build.VERSION_CODES.R = API 30. According to the documentation of BiometricPrompt.authenticate(), only BIOMETRIC_STRONG is allowed for devices with API <30.
fun canAuthenticate(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
BiometricManager.from(context)
.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
} else {
BiometricManager.from(context)
.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS // <----- this returns true!
}
}
However, even though a fingerprint is registered in the emulator and BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS, calling keyPairGenerator.initialize() throws java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use.
This is the code (restricted is true so setUserAuthenticationRequired(true) gets set):
private fun initializeKeyPairGenerator(withStrongBox: Boolean = true): KeyPairGenerator {
val keyPairGenerator = KeyPairGenerator.getInstance(keyGenAlgorithm, provider)
try {
val parameterSpec = createParameterSpec(withStrongBox)
keyPairGenerator.initialize(parameterSpec) // <-------- It throws the exception here
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && e is StrongBoxUnavailableException) {
val parameterSpec = createParameterSpec(false)
keyPairGenerator.initialize(parameterSpec)
} else {
throw Exception("Cannot create key", e)
}
}
return keyPairGenerator
}
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
return KeyGenParameterSpec.Builder(alias, purposes)
.run {
setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
setDigests(KeyProperties.DIGEST_SHA256)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
setBlockModes(encryptionBlockMode)
setEncryptionPaddings(encryptionPadding)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setIsStrongBoxBacked(withStrongBox)
}
if (restricted) {
setUserAuthenticationRequired(true)
}
build()
}
}
The issue seems very related to this issue https://issuetracker.google.com/issues/147374428.
Some things I've tried and an ugly way to make it work with two biometric prompts
Setting setUserAuthenticationValidityDurationSeconds(10) on the KeyGenParameterSpec makes keyPairGenerator.initialize() not throw an exception.
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
return KeyGenParameterSpec.Builder(alias, purposes)
.run {
setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
setDigests(KeyProperties.DIGEST_SHA256)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
setBlockModes(encryptionBlockMode)
setEncryptionPaddings(encryptionPadding)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setIsStrongBoxBacked(withStrongBox)
}
if (restricted) {
setUserAuthenticationRequired(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setUserAuthenticationParameters(
0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
)
}
else { // API <= Q
// parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
// parameter "-1" default to AUTH_BIOMETRIC_STRONG
// source: https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
setUserAuthenticationValidityDurationSeconds(10) // <-- Allow device credential authentication
}
}
build()
}
}
However, it instead throws the following exception when calling initSign(privateKey): ((PlatformException(SIGNING_FAILED, User not authenticated, android.security.keystore.UserNotAuthenticatedException: User not authenticated, null)).
Here is the code:
val signature: Signature
get() = Signature.getInstance(signAlgorithm)
.apply {
val privateKey = asymmetricKeyPair.privateKey
initSign(privateKey) <--- Throws an exception
}
This behavior matches with the documentation of setUserAuthenticationValidityDurationSeconds():
Cryptographic operations involving keys which are authorized to be used for a duration of time after a successful user authentication event can only use secure lock screen authentication. These cryptographic operations will throw UserNotAuthenticatedException during initialization if the user needs to be authenticated to proceed.
The documentation continues with:
This situation can be resolved by the user unlocking the secure lock screen of the Android or by going through the confirm credential flow initiated by KeyguardManager.createConfirmDeviceCredentialIntent(CharSequence, CharSequence). Once resolved, initializing a new cryptographic operation using this key (or any other key which is authorized to be used for a fixed duration of time after user authentication) should succeed provided the user authentication flow completed successfully.
Following these instructions to show a biometric prompt and listening to the result before doing initSign(privateKey) makes initSign(privateKey) not throw an exception, if the user authenticates themself in the prompt by fingerprint.
The code:
private fun triggerBiometricPrompt() {
val bio = BiometricAuthenticator()
val intent = bio.createConfirmDeviceCredentialIntent(activity)
activity.startActivityForResult(intent, 0)
}
In the class FlutterFragmentActivity()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (IdNowMethodCallHandler.handler.onActivityResult(requestCode, resultCode, data)) {
return
}
if (resultCode == Activity.RESULT_OK) {
handler.signWithRestrictedKey(handler.methodCall, handler.methodResult) // <-- The result gets handled here
}
super.onActivityResult(requestCode, resultCode, data)
}
However, this means the user needs to authenticate themself twice, as a second prompt of course is shown when calling BiometricPrompt.authenticate().
The code:
private fun authenticate(
activity: FragmentActivity,
promptInfo: BiometricPrompt.PromptInfo = createPromptInfo(),
signature: Signature?,
onError: (Int, CharSequence) -> Unit,
onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit,
) {
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = onError(errorCode, errString)
override fun onAuthenticationFailed() {
// Called when a biometric (e.g. fingerprint, face, etc.) is presented but not recognized as belonging to the user.
// We want to omit it because the fingerprint maybe just failed to be read in which case the user retries.
// Also, after multiple attempts, the user can use credentials instead.
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = onSuccess(result)
}
val executor = ContextCompat.getMainExecutor(activity)
val prompt = BiometricPrompt(activity, executor, callback)
if (signature == null) {
prompt.authenticate(promptInfo) // TODO: We never do this since signature is never null.
} else {
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature)) // <-- Another prompt is shown here to authenticate
}
}
fun createPromptInfo(
title: String = "Authorize",
subtitle: String = "Please, authorise yourself",
description: String = "This is needed to perform cryptographic operations.",
): BiometricPrompt.PromptInfo {
val builder = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setDescription(description)
.setConfirmationRequired(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
builder.apply {
setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
}
} else {
builder.setNegativeButtonText("Cancel")
}
return builder.build()
}
Needing the user to authenticate twice in a row with biometrics is of course a very poor user experience. It won't even work if the user authenticates with device credentials in the first prompt, and I've found no way to hide that option.
Questions
Why does KeyPairGenerator.initialize() throw the exception java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use on emulators with API 29 with fingerprint set up, even though BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS? Is this simply a bug in the Android system?
Is there a way to make keys with setUserAuthenticationRequired(true) (crypto-based authentication) work on API 29 (or APIs <30)?
I am deeply grateful for any help I can get.
Finally found a solution thanks to https://www.iedigital.com/resources/archive/create-rsa-key-pair-on-android/.
Basically, for API <30, the trick is to use keyGuardManager.createConfirmDeviceCredentialIntent() instead of using BiometricPrompt.authenticate(). The article explains it best but here are the basic steps with some code:
Do setUserAuthenticationValidityDurationSeconds(0) when creating the key.
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
val purposes = KeyProperties.PURPOSE_SIGN
return KeyGenParameterSpec.Builder(alias, purposes)
.run {
setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
setDigests(KeyProperties.DIGEST_SHA256)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
setBlockModes(encryptionBlockMode)
setEncryptionPaddings(encryptionPadding)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setIsStrongBoxBacked(withStrongBox)
}
if (restricted) {
setUserAuthenticationRequired(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setUserAuthenticationParameters(
0 /* duration */,
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
)
}
else { // API <= Q
// parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
// parameter "-1" default to AUTH_BIOMETRIC_STRONG
// source: https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
setUserAuthenticationValidityDurationSeconds(0)
}
}
build()
}
}
Use the deprecated keyGuardManager.createConfirmDeviceCredentialIntent() to display a prompt and let the user authenticate themselves.
fun createConfirmDeviceCredentialIntent(context: Context): Intent {
val keyGuardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
return keyGuardManager.createConfirmDeviceCredentialIntent(
"Authorize", // TODO: Add and use Phrase string https://jimplan.atlassian.net/browse/FS-946
"Please, authorise yourself", // TODO: Add and use Phrase string https://jimplan.atlassian.net/browse/FS-946
)
}
Sign the data
fun sign(signature: Signature, data: ByteArray): ByteArray {
val signedData = signature.run {
update(data)
sign()
}
return signedData
}
Annoying that it doesn't work for all APIs to use BiometricPrompt.authenticate(). I wish this was clearer in the documentation!

Google pay Api in Jetpack compose - How to

I can't find resources on the internet on How to achieve the google API integration into an Compose based app.
I need help, especially in the AutoResolveHelper.resolveTask, how to do it in compose.
Thank you for your answers.
(That's mind blowing that there is no more documentation on this API, it's pretty difficult to implement).
Edit :
This is the traditional way to do it ->
private fun requestPayment() {
// Disables the button to prevent multiple clicks.
googlePayButton.isClickable = false
// The price provided to the API should include taxes and shipping.
// This price is not displayed to the user.
val garmentPrice = selectedGarment.getDouble("price")
val priceCents = Math.round(garmentPrice * PaymentsUtil.CENTS.toLong()) + SHIPPING_COST_CENTS
val paymentDataRequestJson = PaymentsUtil.getPaymentDataRequest(priceCents)
if (paymentDataRequestJson == null) {
Log.e("RequestPayment", "Can't fetch payment data request")
return
}
val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString())
// Since loadPaymentData may show the UI asking the user to select a payment method, we use
// AutoResolveHelper to wait for the user interacting with it. Once completed,
// onActivityResult will be called with the result.
if (request != null) {
AutoResolveHelper.resolveTask(
paymentsClient.loadPaymentData(request), this, LOAD_PAYMENT_DATA_REQUEST_CODE)
}
}
/**
* Handle a resolved activity from the Google Pay payment sheet.
*
* #param requestCode Request code originally supplied to AutoResolveHelper in requestPayment().
* #param resultCode Result code returned by the Google Pay API.
* #param data Intent from the Google Pay API containing payment or error data.
* #see [Getting a result
* from an Activity](https://developer.android.com/training/basics/intents/result)
*/
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
// Value passed in AutoResolveHelper
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
when (resultCode) {
RESULT_OK ->
data?.let { intent ->
PaymentData.getFromIntent(intent)?.let(::handlePaymentSuccess)
}
RESULT_CANCELED -> {
// The user cancelled the payment attempt
}
AutoResolveHelper.RESULT_ERROR -> {
AutoResolveHelper.getStatusFromIntent(data)?.let {
handleError(it.statusCode)
}
}
}
// Re-enables the Google Pay payment button.
googlePayButton.isClickable = true
}
}
}
I recently came into this exact same issue. I didn't want to add the code to the activity and make an ugly, unreadable code so I ended up doing this:
In your composable, add this code to the click modifier or whatever:
val task = paymentsClient.loadPaymentData(request)
task.addOnCompleteListener { completedTask ->
if (completedTask.isSuccessful) {
completedTask.result.let{
//Do whatever you want
}
} else {
when (val exception = completedTask.exception) {
is ResolvableApiException -> {
resolvePaymentForResult.launch(
IntentSenderRequest.Builder(exception.resolution).build()
)
}
is ApiException -> {
Log.e("Google Pay API error", "Error code: ${exception.statusCode}, Message: ${exception.message}")
}
else -> {
Log.e("Unexpected non API exception")
}
}
}
}
With resolvePaymentForResult being:
val resolvePaymentForResult = rememberLauncherForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
result: ActivityResult ->
when (result.resultCode) {
RESULT_OK ->
result.data?.let { intent ->
PaymentData.getFromIntent(intent)?.let{
//Do whatever you want
}
}
RESULT_CANCELED -> {
// The user cancelled the payment attempt
}
}
}
You can always move paymentsClient.loadPaymentData(request) to your ViewModel if that's your architecture too!
Hope that will clean up your code a little bit more :)

Patch Android security provider in Flutter

I am building an Android app in Flutter and Fortify code scan reported the warning: The application does not use the Google Play Service Updated Security Provider which may leave it vulnerable to future vulnerabilities in OpenSSL Library.
For a native Android app, I could follow the guide here but how do I fix the warning in a Flutter app?
In your MainActivity, use the code from this snippet by Google
private const val ERROR_DIALOG_REQUEST_CODE = 1
/**
* Sample activity using {#link ProviderInstaller}.
*/
class MainActivity : Activity(), ProviderInstaller.ProviderInstallListener {
private var retryProviderInstall: Boolean = false
//Update the security provider when the activity is created.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ProviderInstaller.installIfNeededAsync(this, this)
}
/**
* This method is only called if the provider is successfully updated
* (or is already up-to-date).
*/
override fun onProviderInstalled() {
// Provider is up-to-date, app can make secure network calls.
}
/**
* This method is called if updating fails; the error code indicates
* whether the error is recoverable.
*/
override fun onProviderInstallFailed(errorCode: Int, recoveryIntent: Intent) {
GoogleApiAvailability.getInstance().apply {
if (isUserResolvableError(errorCode)) {
// Recoverable error. Show a dialog prompting the user to
// install/update/enable Google Play services.
showErrorDialogFragment(this#MainActivity, errorCode, ERROR_DIALOG_REQUEST_CODE) {
// The user chose not to take the recovery action
onProviderInstallerNotAvailable()
}
} else {
onProviderInstallerNotAvailable()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int,
data: Intent) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ERROR_DIALOG_REQUEST_CODE) {
// Adding a fragment via GoogleApiAvailability.showErrorDialogFragment
// before the instance state is restored throws an error. So instead,
// set a flag here, which will cause the fragment to delay until
// onPostResume.
retryProviderInstall = true
}
}
/**
* On resume, check to see if we flagged that we need to reinstall the
* provider.
*/
override fun onPostResume() {
super.onPostResume()
if (retryProviderInstall) {
// We can now safely retry installation.
ProviderInstaller.installIfNeededAsync(this, this)
}
retryProviderInstall = false
}
private fun onProviderInstallerNotAvailable() {
// This is reached if the provider cannot be updated for some reason.
// App should consider all HTTP communication to be vulnerable, and take
// appropriate action.
}
}
Link to android documentation

Android user messaging platform isConsentFormAvailable returns false

I followed this guide https://developers.google.com/admob/ump/android/quick-start and my code looks like this:
private fun checkForConsent() {
/*
val debugSettings = ConsentDebugSettings.Builder(this)
.setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA)
.addTestDeviceHashedId("69887E2CBBE5346EC3B54A3FD207AB41")
.build()
val params = ConsentRequestParameters.Builder()
.setConsentDebugSettings(debugSettings)
.build()
*/
Log.d("adstuff", "check Consent")
val params = ConsentRequestParameters.Builder().build()
// Set tag for under age of consent. Here false means users are not under age
//Log.d("adstuff", params.isTagForUnderAgeOfConsent.toString())
consentInformation = UserMessagingPlatform.getConsentInformation(this)
//consentInformation.reset();
consentInformation.requestConsentInfoUpdate(
this,
params,
object : ConsentInformation.OnConsentInfoUpdateSuccessListener {
override fun onConsentInfoUpdateSuccess() {
// The consent information state was updated.
// You are now ready to check if a form is available.
Log.d("adstuff", "Is consentform available")
Log.d("adstuff", consentInformation.isConsentFormAvailable.toString())
if (consentInformation.isConsentFormAvailable) {
Log.d("adstuff", "Consentform is available")
loadForm()
}
}
},
object : ConsentInformation.OnConsentInfoUpdateFailureListener {
override fun onConsentInfoUpdateFailure(formError: FormError) {
// Handle the error.
Log.d("adstuff", "content info update failure")
Log.d("adstuff", formError.toString())
}
})
}
However, consentInformation.isConsentFormAvailable returns false. I made an AdMob account and a Funding Choices account where I created and published a message of type Consent TCF. I have also added the ca-app-pub ID to the Android Manifest.
Thanks
Edit: If I set the location to the EEA using the debug settings, like in the commented code, the consent info update fails.
In your phone go to Settings > Privacy > Advanced > Ads and toggle off "Opt out of Ads Personalization"

Exception while trying to access Google Fit API - The user must be signed in to make this API call

I am trying to set up a wearable app (for Huawei Watch 2) running on WearOS to provide a sort of continous feed of Heart Rate (BPM) into a Google Fit account, which is read into another smarphone application.
The issue comes when I am trying to set up the account and access the data as it follows:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.BODY_SENSORS),GOOGLE_FIT_PERMISSIONS_REQUEST_CODE)
}
fitnessOptions = FitnessOptions.builder()
.addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ)
.addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_WRITE)
.build()
account = GoogleSignIn.getAccountForExtension(this, fitnessOptions)
if (!GoogleSignIn.hasPermissions(account, fitnessOptions)) {
GoogleSignIn.requestPermissions(
this, // your activity
GOOGLE_FIT_PERMISSIONS_REQUEST_CODE, // e.g. 1
account,
fitnessOptions);
} else {
accessGoogleFit()
}
timer.scheduleAtFixedRate(
object : TimerTask() {
override fun run() {
Log.i("[TimerTask]", "Retrieving data..")
accessGoogleFit()
Log.i("[Account]", "" + account.email)
}
},0, 1000
)
// Enables Always-on
setAmbientEnabled()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (resultCode) {
Activity.RESULT_OK -> when (requestCode) {
GOOGLE_FIT_PERMISSIONS_REQUEST_CODE -> accessGoogleFit()
else -> {}
}
else -> {}
}
}
private fun accessGoogleFit() {
val cal: Calendar = Calendar.getInstance()
val now = Date()
cal.setTime(now)
val endTime: Long = cal.getTimeInMillis()
cal.add(Calendar.DAY_OF_MONTH, -1)
val startTime: Long = cal.getTimeInMillis()
val historyRequest = DataReadRequest.Builder()
.read(DataType.TYPE_HEART_RATE_BPM)
.enableServerQueries()
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.build()
val sourceRequest = DataSourcesRequest.Builder()
.setDataTypes(DataType.TYPE_HEART_RATE_BPM)
.setDataSourceTypes(DataSource.TYPE_RAW, DataSource.TYPE_DERIVED)
.build()
Fitness.getHistoryClient(this,account)
.readData(historyRequest)
.addOnSuccessListener{
response-> txt_GoogleFit_FitData.setText(response.dataSets.get(0).toString())
}
.addOnFailureListener{ e ->
Log.e("[GoogleFIT]", "Find data sources request failed", e)
}
Fitness.getSensorsClient(this, account)
.findDataSources(sourceRequest)
.addOnSuccessListener { dataSources ->
dataSources.forEach {
Log.i("[GoogleFIT]", "Data source found: ${it.streamIdentifier}")
Log.i("[GoogleFIT]", "Data Source type: ${it.dataType.name}")
if (it.dataType == DataType.TYPE_HEART_RATE_BPM) {
Log.i("[GoogleFIT]", "Data source for LOCATION_SAMPLE found!")
}
}
}
.addOnFailureListener { e ->
Log.e("[GoogleFIT]", "Find data sources request failed", e)
}
}
Stack trace of exception:
2021-01-27 17:08:07.032 13743-13767/com.example.watch_bpmupdated2 I/[TimerTask]: Retrieving data..
2021-01-27 17:08:07.036 13743-13767/com.example.watch_bpmupdated2 I/[Account]: <<default account>>
2021-01-27 17:08:07.057 13743-13743/com.example.watch_bpmupdated2 E/[GoogleFIT]: Find data sources request failed
com.google.android.gms.common.api.ApiException: 4: The user must be signed in to make this API call.
at com.google.android.gms.common.internal.ApiExceptionUtil.fromStatus(com.google.android.gms:play-services-base##17.1.0:4)
at com.google.android.gms.common.internal.zai.zaf(com.google.android.gms:play-services-base##17.1.0:2)
at com.google.android.gms.common.internal.zak.onComplete(com.google.android.gms:play-services-base##17.1.0:6)
at com.google.android.gms.common.api.internal.BasePendingResult.zaa(com.google.android.gms:play-services-base##17.1.0:176)
at com.google.android.gms.common.api.internal.BasePendingResult.setResult(com.google.android.gms:play-services-base##17.1.0:135)
at com.google.android.gms.common.api.internal.BaseImplementation$ApiMethodImpl.setFailedResult(com.google.android.gms:play-services-base##17.1.0:29)
at com.google.android.gms.common.api.internal.zad.zaa(com.google.android.gms:play-services-base##17.1.0:9)
at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.zac(com.google.android.gms:play-services-base##17.1.0:175)
at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.onConnectionFailed(com.google.android.gms:play-services-base##17.1.0:79)
at com.google.android.gms.common.internal.zag.onConnectionFailed(com.google.android.gms:play-services-base##17.1.0:2)
at com.google.android.gms.common.internal.BaseGmsClient$zzg.zza(com.google.android.gms:play-services-basement##17.1.1:6)
at com.google.android.gms.common.internal.BaseGmsClient$zza.zza(com.google.android.gms:play-services-basement##17.1.1:25)
at com.google.android.gms.common.internal.BaseGmsClient$zzb.zzo(com.google.android.gms:play-services-basement##17.1.1:11)
at com.google.android.gms.common.internal.BaseGmsClient$zzc.handleMessage(com.google.android.gms:play-services-basement##17.1.1:49)
at android.os.Handler.dispatchMessage(Handler.java:105)
at com.google.android.gms.internal.common.zzi.dispatchMessage(com.google.android.gms:play-services-basement##17.1.1:8)
at android.os.Looper.loop(Looper.java:164)
at android.os.HandlerThread.run(HandlerThread.java:65)
2021-01-27 17:08:07.060 13743-13743/com.example.watch_bpmupdated2 E/[GoogleFIT]: Find data sources request failed
com.google.android.gms.common.api.ApiException: 4: The user must be signed in to make this API call.
at com.google.android.gms.common.internal.ApiExceptionUtil.fromStatus(com.google.android.gms:play-services-base##17.1.0:4)
at com.google.android.gms.common.internal.zai.zaf(com.google.android.gms:play-services-base##17.1.0:2)
at com.google.android.gms.common.internal.zak.onComplete(com.google.android.gms:play-services-base##17.1.0:6)
at com.google.android.gms.common.api.internal.BasePendingResult.zaa(com.google.android.gms:play-services-base##17.1.0:176)
at com.google.android.gms.common.api.internal.BasePendingResult.setResult(com.google.android.gms:play-services-base##17.1.0:135)
at com.google.android.gms.common.api.internal.BaseImplementation$ApiMethodImpl.setFailedResult(com.google.android.gms:play-services-base##17.1.0:29)
at com.google.android.gms.common.api.internal.zad.zaa(com.google.android.gms:play-services-base##17.1.0:9)
at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.zac(com.google.android.gms:play-services-base##17.1.0:175)
at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.onConnectionFailed(com.google.android.gms:play-services-base##17.1.0:79)
at com.google.android.gms.common.internal.zag.onConnectionFailed(com.google.android.gms:play-services-base##17.1.0:2)
at com.google.android.gms.common.internal.BaseGmsClient$zzg.zza(com.google.android.gms:play-services-basement##17.1.1:6)
at com.google.android.gms.common.internal.BaseGmsClient$zza.zza(com.google.android.gms:play-services-basement##17.1.1:25)
at com.google.android.gms.common.internal.BaseGmsClient$zzb.zzo(com.google.android.gms:play-services-basement##17.1.1:11)
at com.google.android.gms.common.internal.BaseGmsClient$zzc.handleMessage(com.google.android.gms:play-services-basement##17.1.1:49)
at android.os.Handler.dispatchMessage(Handler.java:105)
at com.google.android.gms.internal.common.zzi.dispatchMessage(com.google.android.gms:play-services-basement##17.1.1:8)
at android.os.Looper.loop(Looper.java:164)
at android.os.HandlerThread.run(HandlerThread.java:65)
My watch is paired to the smartphone I am using to read the data (I just need faster updates from google fit, and thought about forcing this. Code not available here for that).
The same google fit account is present, logged into Google Fit app and synced on both devices.
Everything runs smoothly on the smartphone (even with the same code), while on the Watch I get the "user must be signed in" exception when setting up the account.
Google services dependencies, application permissions are set the same way on both devices, as well as the required OAuth settings, SHA1 and credentials on the developer console.
Could this be an issue related to the watch->App->Google Fit account synchronization, or am I missing something?
Register the project in the Google Console and add your Google Account as a test user after adding the OAuth 2.0 Client ID for your project.
This should fix the login issue.
Here is how I fixed it:
Verify that you have a test user in Google Cloud with your Email
Verify that you have a OAuth 2.0 Client IDs on Google Cloud Credentials linked with your certificate
Verify that your app certificate on Android using an app like Package Manager correspond to the written above
Make sure that you call GoogleSignIn.hasPermissions before trying to access anything else and that it returns true
Theses checked should fix most errors from Google Fit not giving you any results or keeping you on the loading screen
Also, Google Fit historical requests need to have a good TimeUnit ex for Type_WEIGHT TimeUnit should be MILLISECONDS as any higher will not return any values
ex historical request builder:
DataReadRequest.Builder()
.read(DataType.TYPE_WEIGHT)
.setTimeRange(/* 1st January 2013 */, /* now */, TimeUnut.MILLISECONDS)
.build()

Categories

Resources