diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 6a82e5f..a678108 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + 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 { @@ -227,17 +242,57 @@ class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityR val source = dataPoint.originalDataSource.streamName return mapOf( - "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 { @@ -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) @@ -260,4 +319,27 @@ class FitnessPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, ActivityR pendingResult = null return true } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array?, + 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 + } + } }