Health Connect - programmatically open the app - android

I followed instructions to add HealthConnect service to the app, however, once the user approves requested permissions, there is no easy way for the user to modify them. They have to go to PlayStore, find HealthConnect, and open it (the icon was recently removed from the launcher).
How does one open health connect with Intent in Android? The following code does NOT work once the permissions are approved requestPermissions.launch(PERMISSIONS).
This is how complete code looks like:
class HealthConnectFragment: Fragment(R.layout.fragment_health_connect) {
private val analyticsHelper: analyticsHelper by inject()
private var _binding: FragmentHealthConnectBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
// Create the permissions launcher.
private val requestPermissionActivityContract = createRequestPermissionResultContract()
// Create the permissions launcher.
private val requestPermissions =
registerForActivityResult(requestPermissionActivityContract) { granted ->
if (granted.containsAll(HealthConnectApi.PERMISSIONS)) {
Timber.e("1 ALL PERMISSIONS GRANTED")
// Permissions successfully granted
} else {
// Lack of required permissions
Timber.e("1 LACKING PERMISSIONS")
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentHealthConnectBinding.bind(view)
analyticsHelper.logScreenView(
Event.AppSetup.HEALTH_CONNECT_SETUP_FRAGMENT,
HealthConnectFragment::class.simpleName!!)
// Set click listener depending on the permissions.
binding.connectButton.setOnClickListener {
if (HealthConnectClient.isAvailable(requireContext())) {
Timber.e("YAYYA")
launchHealthConnectPermissions()
} else {
// ...
Timber.e("NAYYA")
}
}
}
private fun launchHealthConnectPermissions() {
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
requestPermissions.launch(healthConnectApi.PERMISSIONS)
}
}
To summarize, when the app has not approved permissions this action takes me to HealthConnect app to approve the permissions. When the app has granted all permission, clicking on the button does nothing. I would like to ALWAYS launch the HealthConnect app.
Thank you

Users can also access Health Connect by navigating to Settings > Apps > Health Connect, or from their Quick Settings menu.
To open the settings with an intent, you can use the following:
val intent = context.packageManager.getLaunchIntentForPackage("com.google.android.apps.healthdata")
if (HealthConnectClient.isAvailable(context) && intent !== null) {
context.startActivity(intent)
}

Related

Firebase Auth emailVerified doesn't updated

hi guys i'm trying to do auto login in my app but before login done i wonder if the user verified his email or no.
the problem : even if i verified my account the code doesn't see this and said false.
and here is my code.
class SignInActivity : BaseActivity<SignInViewModel, ActivitySignInBinding>(), Navigator {
private lateinit var preferenceManger: PreferenceManger
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferenceManger = PreferenceManger(applicationContext)
autoLogin()
binding.vm = viewModel
viewModel.navigator = this
addPrefManger()
}
private fun autoLogin() {
DataUtils.firebaseUser = Firebase.auth.currentUser
if (preferenceManger.getBoolean(Constants.KEY_IS_SIGNED_IN)) {
when {
DataUtils.firebaseUser!!.isEmailVerified -> {
startActivity(Intent(this, HomeActivity::class.java))
finish()
}
else -> {
startActivity(Intent(this, VerificationActivity::class.java))
finish()
}
}
}
}
this line is always false even if i verified my account.
DataUtils.firebaseUser!!.isEmailVerified
While the verification status of the user profile is updated on the server as soon as they've clicked the link, it may take up to an hour before that information is synchronized to the Android app.
If you want to detect the email verification in the app before it is automatically synchronized, you can:
Sign the user out and in again.
Force reloading of the user profile (after the user has clicked the link) by calling reload on the user object. You can put a button in your UI to do this, or automatically call that, for example in the onResume of the activity.
Also see:
How to verify email without the need of signing in again while using FirebaseUI-auth?
Verification email activity not refreshing

Showing permissions rationale in dialog using accompanist permissions

I am struggling with figure out how to close a dialog launched to explain denied permissions.
Using accompanist to ask for permissions:
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner, effect = {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
locationPermissionState.launchPermissionRequest()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
})
In the same composable I launch a dialog depending on denied permissions:
when {
locationPermissionState.status.shouldShowRationale -> {
AlertDialog(
// Dialog to explain to users the permission
)
}
!locationPermissionState.status.isGranted && !locationPermissionState.status.shouldShowRationale -> {
AlertDialog(
// dialog to tell user they need to go to settings to enable
)
}
}
I am stuck figuring out how to close on the dialog when the user click an OK button.
I have tried to use another state that survives recomposition:
val openDialog by remember { mutableStateOf(false) }
....
// if permission state denied
openDialog = true
....
// then in dialog ok
openDialog = false
However when doing that and changing the state of openDialog the function is recomposed. Which just means when I check the permissions state again its still the same and my dialog opens again.
For a general solution handling location permissions requests in Compose you need to keep track of performed permissions requests. Android's permissions request system works in an iterative fashion, and at certain points in this iteration there is no state change to observe and to act upon besides the iteration count. The current Accompanist 0.24.12-rc has a permissions request callback that you can use to do this. Then you can structure your declarative Compose code to take action based on previously observed and saved iteration counts and the current iteration count. (And you could try to abstract it further, creating dedicated states based on iteration count values and differences, but that's not really necessary to make it work; my hope would be that someone will add this in Accompanist at some point.)
For example:
val locationPermissions: List<String> = listOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)
#Stable
interface LocationPermissionsState : MultiplePermissionsState {
/**
* Supplies a well-defined measure of time/progression
*/
val requestCount: UInt
}
#Composable
fun rememberLocationPermissionsState(): LocationPermissionsState {
val requestCountState: MutableState<UInt> = remember { mutableStateOf(0u) }
val multiplePermissionsState: MultiplePermissionsState =
rememberMultiplePermissionsState(locationPermissions) {
if (it.isEmpty()) {
// BUG in accompanist-permissions library upon configuration change
return#rememberMultiplePermissionsState
}
requestCountState.value++
}
return object : LocationPermissionsState, MultiplePermissionsState by multiplePermissionsState {
override val requestCount: UInt by requestCountState
}
}
You can see this solution with a little more context at https://github.com/google/accompanist/issues/819

Android Permissions Helper Functions

I have an activity that requires camera permission.
this activity can be called from several user configurable places in the app.
The rationale dialog and permission dialog themselves should be shown before the activity opens.
right now I am trying to handle these dialogs in some kind of extension function.
fun handlePermissions(context: Context, required_permissions: Array<String>, activity: FragmentActivity?, fragment: Fragment?): Boolean {
var isGranted = allPermissionsGranted(context, required_permissions)
if (!isGranted) {
//null here is where I used to pass my listener which was the calling fragment previously that implemented an interface
val dialog = DialogPermissionFragment(null, DialogPermissionFragment.PermissionType.QR)
activity?.supportFragmentManager?.let { dialog.show(it, "") }
//get result from dialog? how?
//if accepted launch actual permission request
fragment?.registerForActivityResult(ActivityResultContracts.RequestPermission()) { success ->
isGranted = success
}?.launch(android.Manifest.permission.CAMERA)
}
return isGranted
}
But I am having trouble to get the dialog results back from the rationale/explanation dialog.
My research until now brought me to maybe using a higher order function, to pass a function variable to the dialog fragment that returns a Boolean value to the helper function. But I am absolutely unsure if thats the right thing.
I thought using my own code would be cleaner and less overhead, could I achieve this easier when using a framework like eazy-permissions? (is Dexter still recommendable since its no longer under development)
is that even a viable thing I am trying to achieve here?
One approach that I've implemented and seems viable to use is this:
Class PermissionsHelper
class PermissionsHelper(private val activity: Activity) {
fun requestPermissionsFromDevice(
arrayPermissions: Array<String>, permissionsResultInterface: PermissionsResultInterface
) {
setPermissionResultInterface(permissionsResultInterface)
getMyPermissionRequestActivity().launch(arrayPermissions)
}
fun checkPermissionsFromDevice(permission: String): Boolean {
return ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
Class PermissionsResultInterface to the helper class be able to communicate with the activity.
interface PermissionsResultInterface {
fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>)
}
Usage with this approach to remove all files from app directory:
private fun clearFiles(firstCall: Boolean = false) {
if (verifyStoragePermissions(firstCall)) {
val dir = File(getExternalFilesDir(null).toString())
removeFileOrDirectory(dir)
Toast.makeText(
applicationContext,
resources.getString(R.string.done),
Toast.LENGTH_SHORT
).show()
}
}
private fun verifyStoragePermissions(firstCall: Boolean = false): Boolean {
val arrayListPermissions = arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
for (permission in arrayListPermissions) {
if (!PermissionsHelper(this).checkPermissionsFromDevice(permission)) {
if (firstCall) PermissionsHelper(this)
.requestPermissionsFromDevice(arrayListPermissions, this)
else PermissionsDialogs(this).showPermissionsErrorDialog()
return false
}
}
return true
}
override fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>) {
clearFiles()
}
With this approach you are able to call the permissions helper and using the result interface, after each of the answers from user, decide wether you still need to make a call for permissions or show a dialog to him.
If you need any help don't hesitate to contact me.

Google Fit API does not work in internal testing

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

How to direct users for enabling accessibility service for my app

I know It's impossible to enable the Accessibility service for apps programmatically, so I'd like to direct users to this screen:
System settings --> Accessibility --> app name --> enable/disable screen.
Is that possible ?
You can get them to the Accessibility screen on most devices using ACTION_ACCESSIBILITY_SETTINGS. However:
that may not work on all devices, so you will want to just send them to Settings as a fallback, if you get an ActivityNotFoundException
there is no way to get them straight to any given app, let alone the enable/disable screen
You can at least make it reach the app, making the app item blink. It should work for most devices, or at least those that are like of Pixel devices:
fun <T : AccessibilityService> getRequestAccessibilityPermissionIntents(context: Context, accessibilityService: Class<T>): Array<Intent> {
var intent = Intent("com.samsung.accessibility.installed_service")
if (intent.resolveActivity(context.packageManager) == null) {
intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
}
val extraFragmentArgKey = ":settings:fragment_args_key"
val extraShowFragmentArguments = ":settings:show_fragment_args"
val bundle = Bundle()
val showArgs = "${context.packageName}/${accessibilityService.canonicalName!!}"
bundle.putString(extraFragmentArgKey, showArgs)
intent.putExtra(extraFragmentArgKey, showArgs)
intent.putExtra(extraShowFragmentArguments, bundle)
return arrayOf(intent, Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY))
}
Usage:
private fun requestAccessibilityPermission() {
getRequestAccessibilityPermissionIntents(this, MyAccessibilityService::class.java).forEach { intent ->
try {
startActivity(intent)
return
} catch (e: Exception) {
}
}
//TODO do something here in case it failed
}

Categories

Resources