diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 7e7f500..0000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index e30b29b..7318686 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -7,6 +7,7 @@ + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 069b7f2..415d8e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ + android:value="com.samsung.health.step_count" /> () { ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) Log.d("MainActivity", "Result $requestCode") + for (fragment in supportFragmentManager.primaryNavigationFragment!!.childFragmentManager.fragments) { + fragment.onRequestPermissionsResult(requestCode, permissions, grantResults) + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt b/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt new file mode 100644 index 0000000..ced0e76 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt @@ -0,0 +1,21 @@ +package com.dzeio.openhealth.connectors + +import android.app.Activity +import android.content.Intent +import com.dzeio.openhealth.data.weight.Weight + +interface ConnectorInterface { + + val sourceID: String + + /** + * Same as Activity/Fragment onRequestPermissionResult + * + * But it will only be launched if grantResults[0] == PackageManager.PERMISSION_GRANTED + */ + fun onRequestPermissionResult(requestCode: Int, permission: Array, grantResult: IntArray) + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) + + fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt b/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt new file mode 100644 index 0000000..7d8f84a --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt @@ -0,0 +1,5 @@ +package com.dzeio.openhealth.connectors + +enum class DataType { + WEIGHT +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt index b9f6231..c078755 100644 --- a/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt +++ b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt @@ -2,108 +2,51 @@ package com.dzeio.openhealth.connectors import android.Manifest import android.app.Activity +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat -import com.dzeio.openhealth.data.AppDatabase import com.dzeio.openhealth.data.weight.Weight import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.fitness.Fitness import com.google.android.gms.fitness.FitnessOptions import com.google.android.gms.fitness.data.DataPoint import com.google.android.gms.fitness.data.DataSet -import com.google.android.gms.fitness.data.DataSource import com.google.android.gms.fitness.data.DataType import com.google.android.gms.fitness.request.DataReadRequest -import com.google.android.gms.fitness.request.DataSourcesRequest -import com.google.android.gms.fitness.request.OnDataPointListener -import com.google.android.gms.fitness.request.SensorRequest import java.text.DateFormat import java.text.SimpleDateFormat import java.time.* import java.util.* import java.util.concurrent.TimeUnit -enum class ActionRequestCode { - FIND_DATA_SOURCES -} - class GoogleFit( private val activity: Activity, -) { +) : ConnectorInterface { companion object { const val TAG = "GoogleFitConnector" } -// private val fitnessOptions = FitnessOptions.builder() -// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_WRITE) -// -// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_WRITE) -// -// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_WRITE) -// -// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_READ) -// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_WRITE) -// .build() + + override val sourceID: String = "GoogleFit" private val fitnessOptions = FitnessOptions.builder() -// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE) -// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT) -// .addDataType(DataType.TYPE_SLEEP_SEGMENT) -// .addDataType(DataType.TYPE_CALORIES_EXPENDED) -// .addDataType(DataType.TYPE_BASAL_METABOLIC_RATE) -// .addDataType(DataType.TYPE_POWER_SAMPLE) -// .addDataType(DataType.TYPE_HEART_RATE_BPM) -// .addDataType(DataType.TYPE_LOCATION_SAMPLE) .addDataType(DataType.TYPE_WEIGHT) .build() private val runningQOrLater = - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q - - // [START dataPointListener_variable_reference] - // Need to hold a reference to this listener, as it's passed into the "unregister" - // method in order to stop all sensors from sending data to this listener. - private var dataPointListener: OnDataPointListener? = null + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q fun import() { - checkPermissionsAndRun(ActionRequestCode.FIND_DATA_SOURCES) + checkPermissionsAndRun() } - private fun checkPermissionsAndRun(actionRequestCode: ActionRequestCode) { + private fun checkPermissionsAndRun() { if (permissionApproved()) { - signIn(actionRequestCode) + signIn(0) } else { - requestRuntimePermissions(actionRequestCode) - } - } - - private fun requestRuntimePermissions(requestCode: ActionRequestCode) { - val shouldProvideRationale = - ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION) - - // 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.") -// ProgressDialog.show(activity, "Waiting for authorization...", "") - ActivityCompat.requestPermissions(activity, - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - requestCode.ordinal) - } 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(activity, - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - requestCode.ordinal) - } + requestRuntimePermissions() } } @@ -118,15 +61,41 @@ class GoogleFit( return approved } - fun signIn(requestCode: ActionRequestCode) { - if (oAuthPermissionsApproved()) { - performActionForRequestCode(requestCode) + private fun requestRuntimePermissions() { + val shouldProvideRationale = + ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION) + + // 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. + if (shouldProvideRationale) { + Log.i(TAG, "Displaying permission rationale to provide additional context.") +// ProgressDialog.show(activity, "Waiting for authorization...", "") + ActivityCompat.requestPermissions(activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + 0) } 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(activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + 0) + } + } + + private fun signIn(requestCode: Int) { + if (oAuthPermissionsApproved()) { + Log.d("GoogleFitImporter", "Starting Import") + internalImportWeight() + } else { + Log.d("GoogleFitImporter", "Requesting Permission") requestCode.let { GoogleSignIn.requestPermissions( activity, - it.ordinal, + it, getGoogleAccount(), fitnessOptions) + } } } @@ -141,103 +110,7 @@ class GoogleFit( */ private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, 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. - */ - fun performActionForRequestCode(requestCode: ActionRequestCode) = when (requestCode) { - ActionRequestCode.FIND_DATA_SOURCES -> findFitnessDataSources() - } - - /** Finds available data sources and attempts to register on a specific [DataType]. */ - private fun findFitnessDataSources() { // [START find_data_sources] - // Note: Fitness.SensorsApi.findDataSources() requires the ACCESS_FINE_LOCATION permission. - Fitness.getSensorsClient(activity, getGoogleAccount()) - .findDataSources( - DataSourcesRequest.Builder() - .setDataTypes(DataType.TYPE_LOCATION_SAMPLE) - .setDataSourceTypes(DataSource.TYPE_RAW) - .build()) - .addOnSuccessListener { dataSources -> - for (dataSource in dataSources) { - Log.i(TAG, "Data source found: $dataSource") - Log.i(TAG, "Data Source type: " + dataSource.dataType.name) - // Let's register a listener to receive Activity data! - if (dataSource.dataType == DataType.TYPE_LOCATION_SAMPLE && dataPointListener == null) { - Log.i(TAG, "Data source for LOCATION_SAMPLE found! Registering.") - registerFitnessDataListener(dataSource, DataType.TYPE_LOCATION_SAMPLE) - } - } - } - .addOnFailureListener { e -> Log.e(TAG, "failed", e) } - // [END find_data_sources] - } - - /** - * Registers a listener with the Sensors API for the provided [DataSource] and [DataType] combo. - */ - private fun registerFitnessDataListener(dataSource: DataSource, dataType: DataType) { - // [START register_data_listener] - dataPointListener = OnDataPointListener { dataPoint -> - for (field in dataPoint.dataType.fields) { - val value = dataPoint.getValue(field) - Log.i(TAG, "Detected DataPoint field: ${field.name}") - Log.i(TAG, "Detected DataPoint value: $value") - } - } - Fitness.getSensorsClient(activity, getGoogleAccount()) - .add( - SensorRequest.Builder() - .setDataSource(dataSource) // Optional but recommended for custom data sets. - .setDataType(dataType) // Can't be omitted. - .setSamplingRate(10, TimeUnit.SECONDS) - .build(), - dataPointListener!! - ) - .addOnCompleteListener { task -> - if (task.isSuccessful) { - Log.i(TAG, "Listener registered!") - } else { - Log.e(TAG, "Listener not registered.", task.exception) - } - } - // [END register_data_listener] - } - - @RequiresApi(Build.VERSION_CODES.O) - fun getHistory() { - - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - val now = Date() - calendar.time = now - val endTime = calendar.timeInMillis - calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today - val startTime = calendar.timeInMillis - val readRequest = DataReadRequest.Builder() - .aggregate(DataType.AGGREGATE_CALORIES_EXPENDED) - .bucketByActivityType(1, TimeUnit.SECONDS) - .setTimeRange(startTime, endTime, TimeUnit.SECONDS) - .build() - - Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions)) - .readData(readRequest) - .addOnSuccessListener { response -> - // The aggregate query puts datasets into buckets, so flatten into a - // single list of datasets - for (dataSet in response.buckets.flatMap { it.dataSets }) { - dumpDataSet(dataSet) - } - } - .addOnFailureListener { e -> - Log.w(TAG,"There was an error reading data from Google Fit", e) - } - - } - - fun importWeight(callback : () -> Unit) { + private fun internalImportWeight() { val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) val now = Date() @@ -257,7 +130,7 @@ class GoogleFit( // In this example, it's very unlikely that the request is for several hundred // datapoints each consisting of a few steps and a timestamp. The more likely // scenario is wanting to see how many steps were walked per day, for 7 days. -// .aggregate() +// .aggregate(DataType.AGGREGATE_WEIGHT_SUMMARY) .read(DataType.TYPE_WEIGHT) // Analogous to a "Group By" in SQL, defines how data should be aggregated. @@ -267,110 +140,68 @@ class GoogleFit( // .bucketByActivityType(1, TimeUnit.SECONDS) // .bucketBySession() .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) - .build(), callback) + .build()) } - private fun runRequest(request: DataReadRequest, callback: () -> Unit) { + private fun runRequest(request: DataReadRequest) { Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions)) .readData(request) .addOnSuccessListener { response -> // The aggregate query puts datasets into buckets, so flatten into a // single list of datasets - Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size}") + Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size} ${response.status}") for (dataSet in response.dataSets) { dumpDataSet(dataSet) } - for (dataSet in response.buckets.flatMap { it.dataSets }) { - dumpDataSet(dataSet) - } - callback.invoke() +// for (dataSet in response.buckets.flatMap { it.dataSets }) { +// dumpDataSet(dataSet) +// } } .addOnFailureListener { e -> Log.w(TAG,"There was an error reading data from Google Fit", e) - callback.invoke() } } private fun dumpDataSet(dataSet: DataSet) { Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}") - for (dp in dataSet.dataPoints) { + dataSet.dataPoints.forEachIndexed { index, dp -> val weight = Weight() Log.i(TAG,"Data point:") Log.i(TAG,"\tType: ${dp.dataType.name}") Log.i(TAG,"\tStart: ${dp.getStartTimeString()}") Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}") - weight.timestamp = dp.getStartTime(TimeUnit.SECONDS) + weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS) weight.source = "GoogleFit" for (field in dp.dataType.fields) { weight.weight = dp.getValue(field).asFloat() Log.i(TAG,"\tField: ${field.name.toString()} Value: ${dp.getValue(field)}") + callback(weight, (index + 1) == dataSet.dataPoints.size) } - // AppDatabase.getInstance(activity).weightDao().insert(weight) } } - fun DataPoint.getStartTimeString(): String = - SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE) - .format(Date(this.getStartTime(TimeUnit.SECONDS) * 1000L)) + fun DataPoint.getStartTimeString(): String = Date(this.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString() - fun DataPoint.getEndTimeString(): String = - SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE) - .format(Date(this.getEndTime(TimeUnit.SECONDS) * 1000L)) + fun DataPoint.getEndTimeString(): String = Date(this.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString() - - /** Unregisters the listener with the Sensors API. */ - private fun unregisterFitnessDataListener() { - if (dataPointListener == null) { - // This code only activates one listener at a time. If there's no listener, there's - // nothing to unregister. - return - } - // [START unregister_data_listener] - // Waiting isn't actually necessary as the unregister call will complete regardless, - // even if called from within onStop, but a callback can still be added in order to - // inspect the results. - Fitness.getSensorsClient(activity, getGoogleAccount()) - .remove(dataPointListener!!) - .addOnCompleteListener { task -> - if (task.isSuccessful && task.result!!) { - Log.i(TAG, "Listener was removed!") - } else { - Log.i(TAG, "Listener was not removed.") - } - } - // [END unregister_data_listener] + override fun onRequestPermissionResult( + requestCode: Int, + permission: Array, + grantResult: IntArray + ) { + signIn(requestCode) } - /** Returns a [DataReadRequest] for all step count changes in the past week. */ - private fun queryFitnessData(): DataReadRequest { - // [START build_read_data_request] - // Setting a start and end date using a range of 1 week before this moment. - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - val now = Date() - calendar.time = now - val endTime = calendar.timeInMillis - calendar.add(Calendar.YEAR, -1) - val startTime = calendar.timeInMillis + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + signIn(requestCode) + } - val dateFormat = DateFormat.getDateInstance() - Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}") - Log.i(TAG, "Range End: ${dateFormat.format(endTime)}") + private lateinit var callback: (weight: Weight, end: Boolean) -> Unit - return DataReadRequest.Builder() - // The data request can specify multiple data types to return, effectively - // combining multiple data queries into one call. - // In this example, it's very unlikely that the request is for several hundred - // datapoints each consisting of a few steps and a timestamp. The more likely - // scenario is wanting to see how many steps were walked per day, for 7 days. - .aggregate(DataType.TYPE_STEP_COUNT_DELTA) - // Analogous to a "Group By" in SQL, defines how data should be aggregated. - // bucketByTime allows for a time span, whereas bucketBySession would allow - // bucketing by "sessions", which would need to be defined in code. - .bucketByTime(1, TimeUnit.SECONDS) -// .bucketBySession() - .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) - .build() + override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) { + this.callback = callback + checkPermissionsAndRun() } } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt.old b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt.old new file mode 100644 index 0000000..2d5e17c --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt.old @@ -0,0 +1,376 @@ +package com.dzeio.openhealth.connectors + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import com.dzeio.openhealth.data.AppDatabase +import com.dzeio.openhealth.data.weight.Weight +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.fitness.Fitness +import com.google.android.gms.fitness.FitnessOptions +import com.google.android.gms.fitness.data.DataPoint +import com.google.android.gms.fitness.data.DataSet +import com.google.android.gms.fitness.data.DataSource +import com.google.android.gms.fitness.data.DataType +import com.google.android.gms.fitness.request.DataReadRequest +import com.google.android.gms.fitness.request.DataSourcesRequest +import com.google.android.gms.fitness.request.OnDataPointListener +import com.google.android.gms.fitness.request.SensorRequest +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.time.* +import java.util.* +import java.util.concurrent.TimeUnit + +enum class ActionRequestCode { + FIND_DATA_SOURCES +} + +class GoogleFit( + private val activity: Activity, +) { + companion object { + const val TAG = "GoogleFitConnector" + } +// private val fitnessOptions = FitnessOptions.builder() +// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_READ) +// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_WRITE) +// +// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ) +// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_WRITE) +// +// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_READ) +// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_WRITE) +// +// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_READ) +// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_WRITE) +// .build() + + private val fitnessOptions = FitnessOptions.builder() +// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE) +// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT) +// .addDataType(DataType.TYPE_SLEEP_SEGMENT) +// .addDataType(DataType.TYPE_CALORIES_EXPENDED) +// .addDataType(DataType.TYPE_BASAL_METABOLIC_RATE) +// .addDataType(DataType.TYPE_POWER_SAMPLE) +// .addDataType(DataType.TYPE_HEART_RATE_BPM) +// .addDataType(DataType.TYPE_LOCATION_SAMPLE) + .addDataType(DataType.TYPE_WEIGHT) + .build() + + private val runningQOrLater = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + + // [START dataPointListener_variable_reference] + // Need to hold a reference to this listener, as it's passed into the "unregister" + // method in order to stop all sensors from sending data to this listener. + private var dataPointListener: OnDataPointListener? = null + + fun import() { + checkPermissionsAndRun(ActionRequestCode.FIND_DATA_SOURCES) + } + + private fun checkPermissionsAndRun(actionRequestCode: ActionRequestCode) { + if (permissionApproved()) { + signIn(actionRequestCode) + } else { + requestRuntimePermissions(actionRequestCode) + } + } + + private fun requestRuntimePermissions(requestCode: ActionRequestCode) { + val shouldProvideRationale = + ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION) + + // 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.") +// ProgressDialog.show(activity, "Waiting for authorization...", "") + ActivityCompat.requestPermissions(activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + requestCode.ordinal) + } 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(activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + requestCode.ordinal) + } + } + } + + private fun permissionApproved(): Boolean { + val approved = if (runningQOrLater) { + PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_FINE_LOCATION) + } else { + true + } + return approved + } + + fun signIn(requestCode: ActionRequestCode) { + if (oAuthPermissionsApproved()) { + performActionForRequestCode(requestCode) + } else { + requestCode.let { + GoogleSignIn.requestPermissions( + activity, + it.ordinal, + getGoogleAccount(), fitnessOptions) + } + } + } + + 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(activity, 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. + */ + fun performActionForRequestCode(requestCode: ActionRequestCode) = when (requestCode) { + ActionRequestCode.FIND_DATA_SOURCES -> findFitnessDataSources() + } + + /** Finds available data sources and attempts to register on a specific [DataType]. */ + private fun findFitnessDataSources() { // [START find_data_sources] + // Note: Fitness.SensorsApi.findDataSources() requires the ACCESS_FINE_LOCATION permission. + Fitness.getSensorsClient(activity, getGoogleAccount()) + .findDataSources( + DataSourcesRequest.Builder() + .setDataTypes(DataType.TYPE_LOCATION_SAMPLE) + .setDataSourceTypes(DataSource.TYPE_RAW) + .build()) + .addOnSuccessListener { dataSources -> + for (dataSource in dataSources) { + Log.i(TAG, "Data source found: $dataSource") + Log.i(TAG, "Data Source type: " + dataSource.dataType.name) + // Let's register a listener to receive Activity data! + if (dataSource.dataType == DataType.TYPE_LOCATION_SAMPLE && dataPointListener == null) { + Log.i(TAG, "Data source for LOCATION_SAMPLE found! Registering.") + registerFitnessDataListener(dataSource, DataType.TYPE_LOCATION_SAMPLE) + } + } + } + .addOnFailureListener { e -> Log.e(TAG, "failed", e) } + // [END find_data_sources] + } + + /** + * Registers a listener with the Sensors API for the provided [DataSource] and [DataType] combo. + */ + private fun registerFitnessDataListener(dataSource: DataSource, dataType: DataType) { + // [START register_data_listener] + dataPointListener = OnDataPointListener { dataPoint -> + for (field in dataPoint.dataType.fields) { + val value = dataPoint.getValue(field) + Log.i(TAG, "Detected DataPoint field: ${field.name}") + Log.i(TAG, "Detected DataPoint value: $value") + } + } + Fitness.getSensorsClient(activity, getGoogleAccount()) + .add( + SensorRequest.Builder() + .setDataSource(dataSource) // Optional but recommended for custom data sets. + .setDataType(dataType) // Can't be omitted. + .setSamplingRate(10, TimeUnit.SECONDS) + .build(), + dataPointListener!! + ) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + Log.i(TAG, "Listener registered!") + } else { + Log.e(TAG, "Listener not registered.", task.exception) + } + } + // [END register_data_listener] + } + + @RequiresApi(Build.VERSION_CODES.O) + fun getHistory() { + + val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + val now = Date() + calendar.time = now + val endTime = calendar.timeInMillis + calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today + val startTime = calendar.timeInMillis + val readRequest = DataReadRequest.Builder() + .aggregate(DataType.AGGREGATE_CALORIES_EXPENDED) + .bucketByActivityType(1, TimeUnit.SECONDS) + .setTimeRange(startTime, endTime, TimeUnit.SECONDS) + .build() + + Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions)) + .readData(readRequest) + .addOnSuccessListener { response -> + // The aggregate query puts datasets into buckets, so flatten into a + // single list of datasets + for (dataSet in response.buckets.flatMap { it.dataSets }) { + dumpDataSet(dataSet) + } + } + .addOnFailureListener { e -> + Log.w(TAG,"There was an error reading data from Google Fit", e) + } + + } + + fun importWeight(callback : () -> Unit) { + + val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + val now = Date() + calendar.time = now + val endTime = calendar.timeInMillis + calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today + val startTime = calendar.timeInMillis + + val dateFormat = DateFormat.getDateInstance() + Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}") + Log.i(TAG, "Range End: ${dateFormat.format(endTime)}") + + + runRequest(DataReadRequest.Builder() + // The data request can specify multiple data types to return, effectively + // combining multiple data queries into one call. + // In this example, it's very unlikely that the request is for several hundred + // datapoints each consisting of a few steps and a timestamp. The more likely + // scenario is wanting to see how many steps were walked per day, for 7 days. +// .aggregate() + .read(DataType.TYPE_WEIGHT) + + // Analogous to a "Group By" in SQL, defines how data should be aggregated. + // bucketByTime allows for a time span, whereas bucketBySession would allow + // bucketing by "sessions", which would need to be defined in code. +// .bucketByTime(1, TimeUnit.MINUTES) +// .bucketByActivityType(1, TimeUnit.SECONDS) +// .bucketBySession() + .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) + .build(), callback) + + } + + private fun runRequest(request: DataReadRequest, callback: () -> Unit) { + Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions)) + .readData(request) + .addOnSuccessListener { response -> + // The aggregate query puts datasets into buckets, so flatten into a + // single list of datasets + Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size}") + for (dataSet in response.dataSets) { + dumpDataSet(dataSet) + } + for (dataSet in response.buckets.flatMap { it.dataSets }) { + dumpDataSet(dataSet) + } + callback.invoke() + } + .addOnFailureListener { e -> + Log.w(TAG,"There was an error reading data from Google Fit", e) + callback.invoke() + } + } + + private fun dumpDataSet(dataSet: DataSet) { + Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}") + for (dp in dataSet.dataPoints) { + val weight = Weight() + Log.i(TAG,"Data point:") + Log.i(TAG,"\tType: ${dp.dataType.name}") + Log.i(TAG,"\tStart: ${dp.getStartTimeString()}") + Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}") + weight.timestamp = dp.getStartTime(TimeUnit.SECONDS) + weight.source = "GoogleFit" + for (field in dp.dataType.fields) { + weight.weight = dp.getValue(field).asFloat() + Log.i(TAG,"\tField: ${field.name.toString()} Value: ${dp.getValue(field)}") + } + // AppDatabase.getInstance(activity).weightDao().insert(weight) + } + } + + fun DataPoint.getStartTimeString(): String = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE) + .format(Date(this.getStartTime(TimeUnit.SECONDS) * 1000L)) + + fun DataPoint.getEndTimeString(): String = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE) + .format(Date(this.getEndTime(TimeUnit.SECONDS) * 1000L)) + + + /** Unregisters the listener with the Sensors API. */ + private fun unregisterFitnessDataListener() { + if (dataPointListener == null) { + // This code only activates one listener at a time. If there's no listener, there's + // nothing to unregister. + return + } + // [START unregister_data_listener] + // Waiting isn't actually necessary as the unregister call will complete regardless, + // even if called from within onStop, but a callback can still be added in order to + // inspect the results. + Fitness.getSensorsClient(activity, getGoogleAccount()) + .remove(dataPointListener!!) + .addOnCompleteListener { task -> + if (task.isSuccessful && task.result!!) { + Log.i(TAG, "Listener was removed!") + } else { + Log.i(TAG, "Listener was not removed.") + } + } + // [END unregister_data_listener] + } + + /** Returns a [DataReadRequest] for all step count changes in the past week. */ + private fun queryFitnessData(): DataReadRequest { + // [START build_read_data_request] + // Setting a start and end date using a range of 1 week before this moment. + val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + val now = Date() + calendar.time = now + val endTime = calendar.timeInMillis + calendar.add(Calendar.YEAR, -1) + val startTime = calendar.timeInMillis + + val dateFormat = DateFormat.getDateInstance() + Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}") + Log.i(TAG, "Range End: ${dateFormat.format(endTime)}") + + return DataReadRequest.Builder() + // The data request can specify multiple data types to return, effectively + // combining multiple data queries into one call. + // In this example, it's very unlikely that the request is for several hundred + // datapoints each consisting of a few steps and a timestamp. The more likely + // scenario is wanting to see how many steps were walked per day, for 7 days. + .aggregate(DataType.TYPE_STEP_COUNT_DELTA) + // Analogous to a "Group By" in SQL, defines how data should be aggregated. + // bucketByTime allows for a time span, whereas bucketBySession would allow + // bucketing by "sessions", which would need to be defined in code. + .bucketByTime(1, TimeUnit.SECONDS) +// .bucketBySession() + .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) + .build() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt index a6e080e..01e5501 100644 --- a/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt +++ b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt @@ -1,134 +1,107 @@ package com.dzeio.openhealth.connectors.samsunghealth import android.app.Activity -import android.app.AlertDialog -import android.content.DialogInterface +import android.content.Intent +import android.os.Handler +import android.os.Looper import android.util.Log +import com.dzeio.openhealth.MainActivity +import com.dzeio.openhealth.connectors.ConnectorInterface +import com.dzeio.openhealth.data.weight.Weight import com.samsung.android.sdk.healthdata.* +import com.samsung.android.sdk.healthdata.HealthConstants.StepCount import com.samsung.android.sdk.healthdata.HealthDataStore.ConnectionListener import com.samsung.android.sdk.healthdata.HealthPermissionManager.* -import com.samsung.android.sdk.healthdata.HealthResultHolder.ResultListener -import java.lang.Boolean -import kotlin.Exception +import java.util.* class SamsungHealth( private val context: Activity -) { +) : ConnectorInterface { + companion object { const val TAG = "SamsungHealthConnector" } - private val store: HealthDataStore get() = _store!! - private var _store: HealthDataStore? = null - private val keySet: HashSet = HashSet() - private val permissionListener: ResultListener = - ResultListener { result -> - Log.d(TAG, "Permission callback is received.") - val resultMap: Map = result!!.resultMap - - if (resultMap.containsValue(Boolean.FALSE)) { - // Requesting permission fails - Log.d(TAG, "Requesting permission fails") - - } else { - // Get the current step count and display it - getStepCount() - } - } - - private val connectionListener: ConnectionListener = object : ConnectionListener { + private val listener = object : ConnectionListener { override fun onConnected() { - Log.d(TAG, "Health data service is connected.") - val pmsManager = HealthPermissionManager(store) - try { - val resultMap = pmsManager.isPermissionAcquired(keySet) - - if (resultMap.containsValue(Boolean.FALSE)) { - // Request the permission for reading step counts if it is not acquired - pmsManager.requestPermissions(keySet, context) - .setResultListener(permissionListener) - } else { - // Get the current step count and display it - getStepCount() - } - } catch (e: Exception) { - Log.e(TAG, e.javaClass.name + " - " + e.message) - Log.e(TAG, "Permission setting fails.") + Log.d(TAG, "Connected!") + if (isPermissionAcquired()) { + reporter.start() + } else { + requestPermission() } } - - override fun onConnectionFailed(error: HealthConnectionErrorResult) { + override fun onConnectionFailed(p0: HealthConnectionErrorResult?) { Log.d(TAG, "Health data service is not available.") - showConnectionFailureDialog(error) } override fun onDisconnected() { Log.d(TAG, "Health data service is disconnected.") } + } - init { - //keySet = HashSet() - keySet.add(PermissionKey(HealthConstants.Weight.HEALTH_DATA_TYPE, PermissionType.READ)) + private val store : HealthDataStore = HealthDataStore(context, listener) - _store = HealthDataStore(context, connectionListener) + private fun isPermissionAcquired(): Boolean { + val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ) + val pmsManager = HealthPermissionManager(store) + try { + // Check whether the permissions that this application needs are acquired + val resultMap = pmsManager.isPermissionAcquired(setOf(permKey)) + return !resultMap.containsValue(java.lang.Boolean.FALSE) + } catch (e: java.lang.Exception) { + Log.e(TAG, "Permission request fails.", e) + } + return false } - fun test() { + private fun requestPermission() { + val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ) + val pmsManager = HealthPermissionManager(store) + try { + // Show user permission UI for allowing user to change options + pmsManager.requestPermissions(setOf(permKey), context) + .setResultListener { result: PermissionResult -> + Log.d(TAG, "Permission callback is received.") + val resultMap = + result.resultMap + if (resultMap.containsValue(java.lang.Boolean.FALSE)) { + Log.d(TAG, "No Data???") + } else { + // Get the current step count and display it + reporter.start() + } + } + } catch (e: Exception) { + Log.e(TAG, "Permission setting fails.", e) + } + } + + private val stepCountObserver = object : StepCountReporter.StepCountObserver { + override fun onChanged(count: Int) { + Log.d(TAG, "Step reported : $count") + } + } + + private val reporter = StepCountReporter(store, stepCountObserver, Handler(Looper.getMainLooper())) + + /** + * Connector + */ + + override val sourceID: String = "SamsungHealth" + + override fun onRequestPermissionResult( + requestCode: Int, + permission: Array, + grantResult: IntArray + ) {} + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} + + override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) { store.connectService() } - - private fun getStepCount() { - val resolver = HealthDataResolver(store, null) - val request = HealthDataResolver.ReadRequest.Builder() - .setDataType(HealthConstants.Weight.HEALTH_DATA_TYPE) - .setLocalTimeRange(HealthConstants.Weight.START_TIME, HealthConstants.Weight.TIME_OFFSET, 0, System.currentTimeMillis()) - .build() - - try { - val res = resolver.read(request).await() - - res.use { res1 -> - val iterator = res1.iterator() - if (iterator.hasNext()) { - val data = iterator.next() - val value = data.getFloat(HealthConstants.Weight.WEIGHT) - val time = data.getLong(HealthConstants.Weight.CREATE_TIME) - Log.d(TAG, "Data received! $value $time") - } - } - } catch (e : Exception) { - Log.d(TAG, "Reading failed!") - } - } - - private fun showConnectionFailureDialog(error: HealthConnectionErrorResult) { - val alert: AlertDialog.Builder = AlertDialog.Builder(context) - var message = "Connection with Samsung Health is not available" - if (error.hasResolution()) { - message = when (error.errorCode) { - HealthConnectionErrorResult.PLATFORM_NOT_INSTALLED -> "Please install Samsung Health" - HealthConnectionErrorResult.OLD_VERSION_PLATFORM -> "Please upgrade Samsung Health" - HealthConnectionErrorResult.PLATFORM_DISABLED -> "Please enable Samsung Health" - HealthConnectionErrorResult.USER_AGREEMENT_NEEDED -> "Please agree with Samsung Health policy" - else -> "Please make Samsung Health available" - } - } - alert.setMessage(message) - alert.setPositiveButton("OK", DialogInterface.OnClickListener { dialog, id -> - if (error.hasResolution()) { - error.resolve(context) - } - }) - if (error.hasResolution()) { - alert.setNegativeButton("Cancel", null) - } - alert.show() - } - - - fun destroy() { - store.disconnectService() - } } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/StepCountReporter.kt b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/StepCountReporter.kt new file mode 100644 index 0000000..4545915 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/StepCountReporter.kt @@ -0,0 +1,88 @@ +package com.dzeio.openhealth.connectors.samsunghealth + +import android.os.Handler +import android.util.Log +import com.samsung.android.sdk.healthdata.HealthConstants.StepCount +import com.samsung.android.sdk.healthdata.HealthData +import com.samsung.android.sdk.healthdata.HealthDataObserver +import com.samsung.android.sdk.healthdata.HealthDataResolver +import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateRequest +import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateRequest.AggregateFunction +import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateResult +import com.samsung.android.sdk.healthdata.HealthDataStore +import java.util.* +import java.util.concurrent.TimeUnit + +class StepCountReporter( + private val mStore: HealthDataStore, private val mStepCountObserver: StepCountObserver, + resultHandler: Handler? +) { + private val mHealthDataResolver: HealthDataResolver + private val mHealthDataObserver: HealthDataObserver + fun start() { + // Register an observer to listen changes of step count and get today step count + HealthDataObserver.addObserver(mStore, StepCount.HEALTH_DATA_TYPE, mHealthDataObserver) + readTodayStepCount() + } + + fun stop() { + HealthDataObserver.removeObserver(mStore, mHealthDataObserver) + } + + // Read the today's step count on demand + private fun readTodayStepCount() { + // Set time range from start time of today to the current time + val startTime = getUtcStartOfDay(System.currentTimeMillis(), TimeZone.getDefault()) + val endTime = startTime + TimeUnit.DAYS.toMillis(1) + val request = AggregateRequest.Builder() + .setDataType(StepCount.HEALTH_DATA_TYPE) + .addFunction(AggregateFunction.SUM, StepCount.COUNT, "total_step") + .setLocalTimeRange(StepCount.START_TIME, StepCount.TIME_OFFSET, startTime, endTime) + .build() + try { + mHealthDataResolver.aggregate(request) + .setResultListener { aggregateResult: AggregateResult -> + aggregateResult.use { result -> + val iterator: Iterator = result.iterator() + if (iterator.hasNext()) { + mStepCountObserver.onChanged(iterator.next().getInt("total_step")) + } + } + } + } catch (e: Exception) { + Log.e("APP_TAG", "Getting step count fails.", e) + } + } + + private fun getUtcStartOfDay(time: Long, tz: TimeZone): Long { + val cal = Calendar.getInstance(tz) + cal.timeInMillis = time + val year = cal[Calendar.YEAR] + val month = cal[Calendar.MONTH] + val date = cal[Calendar.DATE] + cal.timeZone = TimeZone.getTimeZone("UTC") + cal[Calendar.YEAR] = year + cal[Calendar.MONTH] = month + cal[Calendar.DATE] = date + cal[Calendar.HOUR_OF_DAY] = 0 + cal[Calendar.MINUTE] = 0 + cal[Calendar.SECOND] = 0 + cal[Calendar.MILLISECOND] = 0 + return cal.timeInMillis + } + + interface StepCountObserver { + fun onChanged(count: Int) + } + + init { + mHealthDataResolver = HealthDataResolver(mStore, resultHandler) + mHealthDataObserver = object : HealthDataObserver(resultHandler) { + // Update the step count when a change event is received + override fun onChange(dataTypeName: String) { + Log.d("APP_TAG", "Observer receives a data changed event") + readTodayStepCount() + } + } + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt index 4ec48f7..8809508 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt @@ -20,4 +20,7 @@ interface WeightDao : BaseDao { @Query("Select * FROM Weight WHERE id=(SELECT max(id) FROM Weight)") fun last(): Flow + + @Query("DELETE FROM Weight where source = :source") + suspend fun deleteFromSource(source: String) } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt index ac68454..f17a720 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt @@ -15,4 +15,5 @@ class WeightRepository @Inject constructor( suspend fun addWeight(weight: Weight) = weightDao.insert(weight) suspend fun deleteWeight(weight: Weight) = weightDao.delete(weight) + suspend fun deleteFromSource(source: String) = weightDao.deleteFromSource(source) } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportViewModel.kt deleted file mode 100644 index f66033f..0000000 --- a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.dzeio.openhealth.ui.main.import - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -//import com.dzeio.openhealth.connectors.GoogleFit - -class ImportViewModel : ViewModel() { - - val text = MutableLiveData().apply { - value = "This is slideshow Fragment" - } - val importProgress = MutableLiveData().apply { - value = 0 - } - // If -1 progress is undetermined - // If 0 no progress bar - // Else progress bar - val importProgressTotal = MutableLiveData().apply { - value = 0 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt similarity index 60% rename from app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportFragment.kt rename to app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt index 6801fa6..813cdd2 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt @@ -1,6 +1,7 @@ -package com.dzeio.openhealth.ui.main.import +package com.dzeio.openhealth.ui.main.imports import android.app.ProgressDialog +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -9,53 +10,38 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.RequiresApi -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.dzeio.openhealth.connectors.ActionRequestCode +import androidx.lifecycle.lifecycleScope +import com.dzeio.openhealth.connectors.ConnectorInterface import com.dzeio.openhealth.connectors.GoogleFit //import com.dzeio.openhealth.connectors.GoogleFit import com.dzeio.openhealth.connectors.samsunghealth.SamsungHealth +import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.databinding.FragmentImportBinding +import dagger.hilt.android.AndroidEntryPoint -class ImportFragment : Fragment() { +@AndroidEntryPoint +class ImportFragment : BaseFragment(ImportViewModel::class.java) { companion object { const val TAG = "ImportFragment" } - private var _binding: FragmentImportBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - private lateinit var viewModel: ImportViewModel + override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentImportBinding = FragmentImportBinding::inflate private lateinit var progressDialog: ProgressDialog - private lateinit var fit: GoogleFit + private lateinit var fit: ConnectorInterface - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - viewModel = ViewModelProvider(this).get(ImportViewModel::class.java) - - _binding = FragmentImportBinding.inflate(inflater, container, false) - val root: View = binding.root + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) progressDialog = ProgressDialog(requireContext()) progressDialog.apply { - setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) setCancelable(false) + setTitle("Importing from source...") } - return root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) binding.importGoogleFit.setOnClickListener { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { importFromGoogleFit() @@ -66,29 +52,67 @@ class ImportFragment : Fragment() { } } - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - @RequiresApi(Build.VERSION_CODES.O) - fun importFromGoogleFit() { - viewModel.importProgressTotal.postValue(-1) + private fun importFromGoogleFit() { + progressDialog.show() fit = GoogleFit(requireActivity()) - progressDialog.show() - fit.import() - fit.getHistory() -// google.importWeight { -// progressDialog.dismiss() -// } + var imported = 0 + + lifecycleScope.launchWhenStarted { + viewModel.deleteFromSource(fit.sourceID) + }.invokeOnCompletion { + //progressDialog.show() + fit.importWeight { weight, end -> + Log.d("Importer", "Importing $weight") + weight.source = fit.sourceID + progressDialog.setTitle("Importing from source... ${++imported}") + lifecycleScope.launchWhenStarted { + viewModel.importWeight(weight) + } + if (end) { + Log.d("Importer", "Finished Importing") + progressDialog.dismiss() + return@importWeight + } + + } + } + + } fun importFromSamsungHealth() { - SamsungHealth(requireActivity()) - .test() + progressDialog.show() + fit = SamsungHealth(requireActivity()) + + var imported = 0 + + lifecycleScope.launchWhenStarted { + viewModel.deleteFromSource(fit.sourceID) + }.invokeOnCompletion { + //progressDialog.show() + fit.importWeight { weight, end -> + Log.d("Importer", "Importing $weight") + weight.source = fit.sourceID + progressDialog.setTitle("Importing from source... ${++imported}") + lifecycleScope.launchWhenStarted { + viewModel.importWeight(weight) + } + if (end) { + Log.d("Importer", "Finished Importing") + progressDialog.dismiss() + return@importWeight + } + + } + } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + fit.onActivityResult(requestCode, resultCode, data) + } + + @RequiresApi(Build.VERSION_CODES.O) override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when { @@ -100,11 +124,7 @@ class ImportFragment : Fragment() { grantResults[0] == PackageManager.PERMISSION_GRANTED -> { Log.d(TAG, "Granted") - // Permission was granted. - val fitActionRequestCode = ActionRequestCode.values()[requestCode] - fitActionRequestCode.let { - fit.signIn(ActionRequestCode.FIND_DATA_SOURCES) - } + fit.onRequestPermissionResult(requestCode, permissions, grantResults) } else -> { // Permission denied. diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportViewModel.kt new file mode 100644 index 0000000..ed3651c --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportViewModel.kt @@ -0,0 +1,32 @@ +package com.dzeio.openhealth.ui.main.imports + +import androidx.lifecycle.MutableLiveData +import com.dzeio.openhealth.core.BaseViewModel +import com.dzeio.openhealth.data.weight.Weight +import com.dzeio.openhealth.data.weight.WeightRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import java.security.CodeSource +import javax.inject.Inject + + +@HiltViewModel +class ImportViewModel @Inject internal constructor( + private val weightRepository: WeightRepository +) : BaseViewModel() { + + val text = MutableLiveData().apply { + value = "This is slideshow Fragment" + } + val importProgress = MutableLiveData().apply { + value = 0 + } + // If -1 progress is undetermined + // If 0 no progress bar + // Else progress bar + val importProgressTotal = MutableLiveData().apply { + value = 0 + } + + suspend fun importWeight(weight: Weight) = weightRepository.addWeight(weight) + suspend fun deleteFromSource(source: String) = weightRepository.deleteFromSource(source) +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt index 24ff6d8..3f649d7 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt @@ -10,11 +10,14 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.dzeio.openhealth.adapters.WeightAdapter import com.dzeio.openhealth.core.BaseFragment +import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.databinding.FragmentListWeightBinding import com.dzeio.openhealth.ui.dialogs.EditWeightDialog import com.dzeio.openhealth.ui.main.home.HomeViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import java.util.* @AndroidEntryPoint class ListWeightFragment : BaseFragment(HomeViewModel::class.java) { @@ -37,8 +40,10 @@ class ListWeightFragment : BaseFragment if (o1.timestamp > o2.timestamp) -1 else 1 } + adapter.set(itt) } } diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index 2b068d1..a0037ac 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -4,27 +4,48 @@ android:height="108dp" android:viewportWidth="108" android:viewportHeight="108"> - - - - - - - + + + + + + + + - \ No newline at end of file + android:pathData="M284,162H40M162,40V284" + android:strokeAlpha="0.5" + android:strokeLineJoin="round" + android:strokeWidth="76" + android:fillColor="#00000000" + android:strokeColor="#80E27E" + android:strokeLineCap="round"/> + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_logo_app.xml b/app/src/main/res/drawable/ic_logo_app.xml new file mode 100644 index 0000000..4460be8 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_app.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_logo_app_white.xml b/app/src/main/res/drawable/ic_logo_app_white.xml new file mode 100644 index 0000000..75a9d57 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_app_white.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_logo_background.xml b/app/src/main/res/drawable/ic_logo_background.xml new file mode 100644 index 0000000..918db80 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo_background.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_import.xml b/app/src/main/res/layout/fragment_import.xml index 433c21e..65f44b6 100644 --- a/app/src/main/res/layout/fragment_import.xml +++ b/app/src/main/res/layout/fragment_import.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.main.import.ImportFragment"> + tools:context=".ui.main.imports.ImportFragment"> - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cf..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..28ad9c6 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..a4b162c Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..da66b1b Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..df9c3f2 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1ae011f Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..ead7fc9 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..74ac628 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..9bcd797 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..1487e3f Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..f6274b0 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 78aaf0d..3ae8231 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -28,7 +28,7 @@ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file