Skip to content

Commit

Permalink
[UPDATED] Fixed Android permission acquisition error.
Browse files Browse the repository at this point in the history
- ACTIVITY_RECOGNITION permission
  • Loading branch information
dev-juyoung committed Feb 8, 2021
1 parent 2839674 commit f53bdb8
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 58 deletions.
2 changes: 2 additions & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.juyoung.fitness">

<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>

<application>

<provider
Expand Down
198 changes: 140 additions & 58 deletions android/src/main/kotlin/dev/juyoung/fitness/FitnessPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package dev.juyoung.fitness

import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
Expand All @@ -25,10 +29,12 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
import timber.log.Timber
import java.util.concurrent.TimeUnit

class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityResultListener {
class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityResultListener,
RequestPermissionsResultListener {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var activityBinding: ActivityPluginBinding
Expand All @@ -39,6 +45,7 @@ class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityR
const val CHANNEL = "plugins.juyoung.dev/fitness"

// permission request code
const val ACTIVITY_RECOGNITION_REQUEST_CODE = 3641
const val GOOGLE_FIT_REQUEST_CODE = 2172

// method names
Expand Down Expand Up @@ -105,14 +112,17 @@ class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityR
private fun attachToActivity(binding: ActivityPluginBinding) {
activityBinding = binding.also {
it.addActivityResultListener(this)
it.addRequestPermissionsResultListener(this)
}
}

private fun disposeActivity() {
activityBinding.removeActivityResultListener(this)
with(activityBinding) {
removeActivityResultListener(this@FitnessPlugin)
removeRequestPermissionsResultListener(this@FitnessPlugin)
}
}


override fun onMethodCall(call: MethodCall, result: Result) {
try {
when (call.method) {
Expand All @@ -124,120 +134,165 @@ class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityR
}
} catch (e: Throwable) {
when (e) {
is MissingArgumentException -> result.error(ERROR_MISSING_REQUIRED_ARGUMENTS, ERROR_MISSING_REQUIRED_ARGUMENTS_MESSAGE, null)
is MissingArgumentException -> result.error(
ERROR_MISSING_REQUIRED_ARGUMENTS,
ERROR_MISSING_REQUIRED_ARGUMENTS_MESSAGE,
null
)
else -> result.error(ERROR_EXCEPTION, e.message, null)
}
}
}

private fun subscribe() {
if (!isAuthorized()) {
if (!isPermissionAcquired()) {
Timber.w("$TAG::subscribe::You cannot subscribe. user has not been authenticated.")
return
}

Fitness.getRecordingClient(context, getFitnessAccount())
.subscribe(DataType.TYPE_STEP_COUNT_CUMULATIVE)
.addOnCompleteListener {
if (it.isSuccessful) {
Timber.i("$TAG::subscribe::Successfully subscribed.")
} else {
Timber.w("$TAG::subscribe::There was a problem subscribing.${it.exception}")
}
.subscribe(DataType.TYPE_STEP_COUNT_CUMULATIVE)
.addOnCompleteListener {
if (it.isSuccessful) {
Timber.i("$TAG::subscribe::Successfully subscribed.")
} else {
Timber.w("$TAG::subscribe::There was a problem subscribing.${it.exception}")
}
}
}

private fun hasPermission(call: MethodCall, result: Result) {
result.success(isAuthorized())
result.success(isPermissionAcquired())
}

private fun requestPermission(call: MethodCall, result: Result) {
pendingResult = result

if (isAuthorized()) {
if (isPermissionAcquired()) {
result.success(true)
pendingResult = null
return
}

GoogleSignIn.requestPermissions(
activityBinding.activity,
GOOGLE_FIT_REQUEST_CODE,
getFitnessAccount(),
getFitnessOptions()
)
requestActivityRecognitionPermission()
}

// Related: https://github.com/android/fit-samples/issues/28
private fun revokePermission(call: MethodCall, result: Result) {
Fitness.getConfigClient(context, getFitnessAccount())
.disableFit()
.continueWithTask {
val signInOptions = GoogleSignInOptions.Builder()
.addExtension(FitnessOptions.builder().build())
.build()
.disableFit()
.continueWithTask {
val signInOptions = GoogleSignInOptions.Builder()
.addExtension(FitnessOptions.builder().build())
.build()

GoogleSignIn.getClient(context, signInOptions).revokeAccess()
}
.addOnSuccessListener { result.success(true) }
.addOnFailureListener {
Timber.e("$TAG::revokePermission::$it")
if (!isAuthorized()) {
result.success(true)
} else {
result.success(false)
}
GoogleSignIn.getClient(context, signInOptions).revokeAccess()
}
.addOnSuccessListener { result.success(true) }
.addOnFailureListener {
Timber.e("$TAG::revokePermission::$it")
if (!isAuthorized()) {
result.success(true)
} else {
result.success(false)
}
}
}

@Throws
private fun read(call: MethodCall, result: Result) {
if (!isAuthorized()) {
if (!isPermissionAcquired()) {
result.error(ERROR_UNAUTHORIZED, ERROR_UNAUTHORIZED_MESSAGE, null)
return
}

val dateFrom = call.getLong(ARG_DATE_FROM) ?: throw MissingArgumentException()
val dateTo = call.getLong(ARG_DATE_TO) ?: throw MissingArgumentException()
val bucketByTime = call.getInt(ARG_BUCKET_BY_TIME) ?: throw MissingArgumentException()
val timeUnit = call.getString(ARG_TIME_UNIT)?.timeUnit ?: throw MissingArgumentException()

val request = DataReadRequest.Builder()
.aggregate(DataType.TYPE_STEP_COUNT_DELTA)
.bucketByTime(bucketByTime, timeUnit)
.setTimeRange(dateFrom, dateTo, TimeUnit.MILLISECONDS)
.enableServerQueries()
.build()
.aggregate(DataType.TYPE_STEP_COUNT_DELTA)
.bucketByTime(bucketByTime, timeUnit)
.setTimeRange(dateFrom, dateTo, TimeUnit.MILLISECONDS)
.enableServerQueries()
.build()

Fitness.getHistoryClient(context, getFitnessAccount())
.readData(request)
.addOnSuccessListener { response ->
(response.dataSets + response.buckets.flatMap { it.dataSets })
.filterNot { it.isEmpty }
.flatMap { it.dataPoints }
.map(::dataPointToMap)
.let(result::success)
}
.addOnFailureListener { result.error(ERROR_EXCEPTION, it.message, null) }
.addOnCanceledListener { result.error(ERROR_REQUEST_CANCELED, ERROR_REQUEST_CANCELED_MESSAGE, null) }
.readData(request)
.addOnSuccessListener { response ->
(response.dataSets + response.buckets.flatMap { it.dataSets })
.filterNot { it.isEmpty }
.flatMap { it.dataPoints }
.map(::dataPointToMap)
.let(result::success)
}
.addOnFailureListener { result.error(ERROR_EXCEPTION, it.message, null) }
.addOnCanceledListener {
result.error(
ERROR_REQUEST_CANCELED,
ERROR_REQUEST_CANCELED_MESSAGE,
null
)
}
}

private fun dataPointToMap(dataPoint: DataPoint): Map<String, Any> {
val field = dataPoint.dataType.fields.first()
val source = dataPoint.originalDataSource.streamName

return mapOf<String, Any>(
"value" to dataPoint.getValue(field).asInt(),
"date_from" to dataPoint.getStartTime(TimeUnit.MILLISECONDS),
"date_to" to dataPoint.getEndTime(TimeUnit.MILLISECONDS),
"source" to source
"value" to dataPoint.getValue(field).asInt(),
"date_from" to dataPoint.getStartTime(TimeUnit.MILLISECONDS),
"date_to" to dataPoint.getEndTime(TimeUnit.MILLISECONDS),
"source" to source
)
}

// Android OS system permission related
private fun hasActivityRecognition(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACTIVITY_RECOGNITION
) == PackageManager.PERMISSION_GRANTED
} else {
return true
}
}

private fun requestActivityRecognitionPermission() {
if (hasActivityRecognition()) {
requestFitnessPermission()
return
}

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
ActivityCompat.requestPermissions(
activityBinding.activity,
arrayOf(Manifest.permission.ACTIVITY_RECOGNITION),
ACTIVITY_RECOGNITION_REQUEST_CODE
)
} else {
requestFitnessPermission()
}
}

// Google Fitness related
private fun requestFitnessPermission() {
GoogleSignIn.requestPermissions(
activityBinding.activity,
GOOGLE_FIT_REQUEST_CODE,
getFitnessAccount(),
getFitnessOptions()
)
}

private fun getFitnessOptions(): FitnessOptions {
return FitnessOptions.builder()
.addDataType(DataType.TYPE_STEP_COUNT_DELTA)
.build()
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_WRITE)
.build()
}

private fun getFitnessAccount(): GoogleSignInAccount {
Expand All @@ -248,6 +303,10 @@ class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityR
return GoogleSignIn.hasPermissions(getFitnessAccount(), getFitnessOptions())
}

private fun isPermissionAcquired(): Boolean {
return hasActivityRecognition() && isAuthorized()
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode != GOOGLE_FIT_REQUEST_CODE || resultCode != Activity.RESULT_OK) {
pendingResult?.success(false)
Expand All @@ -260,4 +319,27 @@ class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityR
pendingResult = null
return true
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>?,
grantResults: IntArray?
): Boolean {
return when (requestCode) {
ACTIVITY_RECOGNITION_REQUEST_CODE -> {
val granted =
grantResults != null && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED

if (granted) {
requestFitnessPermission()
} else {
pendingResult?.success(false)
pendingResult = null
}

true
}
else -> false
}
}
}

0 comments on commit f53bdb8

Please sign in to comment.