mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-24 03:42:13 +00:00
a
This commit is contained in:
parent
a164e23b2d
commit
57beb426fb
123
.idea/codeStyles/Project.xml
generated
Normal file
123
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,123 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
@ -3,6 +3,10 @@
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="..\:/Git/Dzeio/OpenHealth/app/src/main/res/layout/fragment_home.xml" value="0.13489583333333333" />
|
||||
<entry key="..\:/Git/Dzeio/OpenHealth/app/src/main/res/layout/fragment_import.xml" value="0.1" />
|
||||
<entry key="..\:/Git/Dzeio/OpenHealth/app/src/main/res/menu/activity_main_drawer.xml" value="0.10989583333333333" />
|
||||
<entry key="..\:/Git/Dzeio/OpenHealth/app/src/main/res/menu/main.xml" value="0.1875" />
|
||||
<entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/layout/content_main.xml" value="0.5135869565217391" />
|
||||
<entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/layout/fragment_home.xml" value="0.5135869565217391" />
|
||||
</map>
|
||||
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-kapt'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -51,4 +52,30 @@ dependencies {
|
||||
// Google Fit
|
||||
implementation "com.google.android.gms:play-services-fitness:21.0.0"
|
||||
implementation "com.google.android.gms:play-services-auth:20.0.0"
|
||||
|
||||
// Samsung Health
|
||||
implementation files('libs/samsung-health-data-1.5.0.aar')
|
||||
|
||||
// ROOM
|
||||
def room_version = "2.3.0"
|
||||
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
// optional - RxJava2 support for Room
|
||||
implementation "androidx.room:room-rxjava2:$room_version"
|
||||
|
||||
// optional - RxJava3 support for Room
|
||||
implementation "androidx.room:room-rxjava3:$room_version"
|
||||
|
||||
// optional - Guava support for Room, including Optional and ListenableFuture
|
||||
implementation "androidx.room:room-guava:$room_version"
|
||||
|
||||
// optional - Test helpers
|
||||
testImplementation "androidx.room:room-testing:$room_version"
|
||||
|
||||
// optional - Paging 3 Integration
|
||||
implementation "androidx.room:room-paging:2.4.0-rc01"
|
||||
|
||||
|
||||
}
|
@ -6,6 +6,11 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION " />
|
||||
|
||||
<!-- Samsung Health-->
|
||||
<queries>
|
||||
<package android:name="com.sec.android.app.shealth" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@ -13,6 +18,12 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.OpenHealth">
|
||||
|
||||
<!-- Samsung Health-->
|
||||
<meta-data
|
||||
android:name="com.samsung.android.health.permission.read"
|
||||
android:value="com.samsung.health.step_count;com.samsung.shealth.step_daily_trend" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
@ -12,6 +12,8 @@ import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.dzeio.openhealth.databinding.ActivityMainBinding
|
||||
import com.dzeio.openhealth.db.AppDatabase
|
||||
import com.dzeio.openhealth.db.entities.Weight
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
@ -39,12 +41,11 @@ class MainActivity : AppCompatActivity() {
|
||||
// menu should be considered as top level destinations.
|
||||
appBarConfiguration = AppBarConfiguration(
|
||||
setOf(
|
||||
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow
|
||||
R.id.nav_home, R.id.nav_gallery, R.id.nav_import
|
||||
), drawerLayout
|
||||
)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
navView.setupWithNavController(navController)
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
|
@ -7,6 +7,8 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.dzeio.openhealth.db.AppDatabase
|
||||
import com.dzeio.openhealth.db.entities.Weight
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.fitness.Fitness
|
||||
import com.google.android.gms.fitness.FitnessOptions
|
||||
@ -18,9 +20,10 @@ 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.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
enum class ActionRequestCode {
|
||||
@ -194,49 +197,115 @@ class GoogleFit(
|
||||
fun getHistory() {
|
||||
|
||||
val endTime = LocalDateTime.now().atZone(ZoneId.systemDefault())
|
||||
val startTime = LocalDateTime.MIN.atZone(ZoneId.systemDefault())
|
||||
val readRequest = DataReadRequest.Builder()
|
||||
.aggregate(DataType.AGGREGATE_CALORIES_EXPENDED)
|
||||
.bucketByActivityType(1, TimeUnit.SECONDS)
|
||||
.setTimeRange(endTime.minusWeeks(1).toEpochSecond(), endTime.toEpochSecond(), TimeUnit.SECONDS)
|
||||
.setTimeRange(startTime.toEpochSecond(), endTime.toEpochSecond(), 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 }) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
dumpDataSet(dataSet)
|
||||
} else {
|
||||
Log.e(TAG, "DUMB SHIT")
|
||||
}
|
||||
dumpDataSet(dataSet)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.w(TAG,"There was an error reading data from Google Fit", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @RequiresApi(Build.VERSION_CODES.O)
|
||||
// fun importStepCount() {
|
||||
//
|
||||
// runRequest(queryFitnessData())
|
||||
//
|
||||
// }
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun dumpDataSet(dataSet: DataSet) {
|
||||
Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name}")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun DataPoint.getStartTimeString() = Instant.ofEpochSecond(this.getStartTime(TimeUnit.SECONDS))
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime().toString()
|
||||
fun DataPoint.getStartTimeString(): String =
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE)
|
||||
.format(Date(this.getStartTime(TimeUnit.SECONDS) * 1000L))
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun DataPoint.getEndTimeString() = Instant.ofEpochSecond(this.getEndTime(TimeUnit.SECONDS))
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime().toString()
|
||||
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. */
|
||||
@ -261,4 +330,36 @@ class GoogleFit(
|
||||
}
|
||||
// [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()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package com.dzeio.openhealth.connectors.samsunghealth
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.util.Log
|
||||
import com.samsung.android.sdk.healthdata.*
|
||||
|
||||
class SamsungHealth(
|
||||
private val activity: Activity
|
||||
) {
|
||||
|
||||
private var currentStartTime: Long = 0
|
||||
private var isFinishing = false
|
||||
|
||||
init {
|
||||
currentStartTime = StepCountReader.TODAY_START_UTC_TIME
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "SamsungHealth"
|
||||
}
|
||||
|
||||
// private val binningListAdapter: StepBinningData by lazy { StepBinningData() }
|
||||
|
||||
private val healthDataStore: HealthDataStore by lazy { HealthDataStore(activity, connectionListener) }
|
||||
private val stepCountReader: StepCountReader by lazy { StepCountReader(healthDataStore, stepCountObserver) }
|
||||
|
||||
private val stepCountObserver: StepCountObserver = object : StepCountObserver {
|
||||
override fun onChanged(count: Int) {
|
||||
Log.d(TAG, "$count")
|
||||
}
|
||||
|
||||
override fun onBinningDataChanged(binningCountList: List<StepBinningData>) {
|
||||
Log.d(TAG, "${binningCountList.size}")
|
||||
}
|
||||
}
|
||||
|
||||
private val connectionListener: HealthDataStore.ConnectionListener = object : HealthDataStore.ConnectionListener {
|
||||
override fun onConnected() {
|
||||
Log.d(TAG, "onConnected")
|
||||
if (checkPermissionsAcquired()) {
|
||||
stepCountReader.requestDailyStepCount(currentStartTime)
|
||||
} else {
|
||||
requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionFailed(error: HealthConnectionErrorResult) {
|
||||
Log.d(TAG, "onConnectionFailed")
|
||||
showConnectionFailureDialog(error)
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
Log.d(TAG, "onDisconnected")
|
||||
if (!isFinishing) {
|
||||
healthDataStore.connectService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConnectionFailureDialog(error: HealthConnectionErrorResult) {
|
||||
if (isFinishing) {
|
||||
return
|
||||
}
|
||||
val alert = AlertDialog.Builder(activity)
|
||||
if (error.hasResolution()) {
|
||||
when (error.errorCode) {
|
||||
HealthConnectionErrorResult.PLATFORM_NOT_INSTALLED -> alert.setMessage("R.string.msg_req_install")
|
||||
HealthConnectionErrorResult.OLD_VERSION_PLATFORM -> alert.setMessage("R.string.msg_req_upgrade")
|
||||
HealthConnectionErrorResult.PLATFORM_DISABLED -> alert.setMessage("R.string.msg_req_enable")
|
||||
HealthConnectionErrorResult.USER_AGREEMENT_NEEDED -> alert.setMessage("R.string.msg_req_agree")
|
||||
else -> alert.setMessage("R.string.msg_req_available")
|
||||
}
|
||||
} else {
|
||||
alert.setMessage("R.string.msg_conn_not_available")
|
||||
}
|
||||
alert.setPositiveButton("R.string.ok") { _, _ ->
|
||||
if (error.hasResolution()) {
|
||||
error.resolve(activity)
|
||||
}
|
||||
}
|
||||
if (error.hasResolution()) {
|
||||
alert.setNegativeButton("R.string.cancel", null)
|
||||
}
|
||||
alert.show()
|
||||
}
|
||||
|
||||
// Check whether the permissions that this application needs are acquired
|
||||
private fun checkPermissionsAcquired(): Boolean {
|
||||
val pmsManager = HealthPermissionManager(healthDataStore)
|
||||
|
||||
// Check whether the permissions that this application needs are acquired
|
||||
return runCatching { pmsManager.isPermissionAcquired(permissionKeySet) }
|
||||
.onFailure { Log.e(TAG, "Permission request fails.", it) }
|
||||
.map { it.values.all { it } }
|
||||
.getOrDefault(false)
|
||||
}
|
||||
|
||||
private fun requestPermission() {
|
||||
val pmsManager = HealthPermissionManager(healthDataStore)
|
||||
|
||||
// Show user permission UI for allowing user to change options
|
||||
runCatching { pmsManager.requestPermissions(permissionKeySet, activity) }
|
||||
.onFailure { Log.e(TAG, "Permission setting fails.", it) }
|
||||
.getOrNull()
|
||||
?.setResultListener(mPermissionListener)
|
||||
}
|
||||
|
||||
private val permissionKeySet: Set<HealthPermissionManager.PermissionKey> =
|
||||
setOf(
|
||||
HealthPermissionManager.PermissionKey(HealthConstants.StepCount.HEALTH_DATA_TYPE, HealthPermissionManager.PermissionType.READ),
|
||||
HealthPermissionManager.PermissionKey(
|
||||
HealthConstants.StepDailyTrend.HEALTH_DATA_TYPE,
|
||||
HealthPermissionManager.PermissionType.READ
|
||||
)
|
||||
)
|
||||
|
||||
private val mPermissionListener = HealthResultHolder.ResultListener<HealthPermissionManager.PermissionResult> { result ->
|
||||
// Show a permission alarm and clear step count if permissions are not acquired
|
||||
if (result.resultMap.values.any { !it }) {
|
||||
// List is now empty
|
||||
showPermissionAlarmDialog()
|
||||
} else {
|
||||
// Get the daily step count of a particular day and display it
|
||||
stepCountReader.requestDailyStepCount(currentStartTime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPermissionAlarmDialog() {
|
||||
if (isFinishing) {
|
||||
return
|
||||
}
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle("R.string.notice")
|
||||
.setMessage("R.string.msg_perm_acquired")
|
||||
.setPositiveButton("R.string.ok", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun importStepCount() {
|
||||
healthDataStore.connectService()
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package com.dzeio.openhealth.connectors.samsunghealth
|
||||
|
||||
data class StepBinningData(var time: String, val count: Int)
|
@ -0,0 +1,8 @@
|
||||
package com.dzeio.openhealth.connectors.samsunghealth
|
||||
|
||||
interface StepCountObserver {
|
||||
|
||||
fun onChanged(count: Int)
|
||||
|
||||
fun onBinningDataChanged(binningCountList: List<StepBinningData>)
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package com.dzeio.openhealth.connectors.samsunghealth
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.samsung.android.sdk.healthdata.HealthConstants.StepCount
|
||||
import com.samsung.android.sdk.healthdata.HealthConstants.StepDailyTrend
|
||||
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.AggregateRequest.TimeGroupUnit
|
||||
import com.samsung.android.sdk.healthdata.HealthDataResolver.Filter
|
||||
import com.samsung.android.sdk.healthdata.HealthDataResolver.ReadRequest
|
||||
import com.samsung.android.sdk.healthdata.HealthDataResolver.SortOrder
|
||||
import com.samsung.android.sdk.healthdata.HealthDataStore
|
||||
import com.samsung.android.sdk.healthdata.HealthDataUtil
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class StepCountReader(
|
||||
store: HealthDataStore,
|
||||
private val observer: StepCountObserver
|
||||
) {
|
||||
private val healthDataResolver: HealthDataResolver = HealthDataResolver(store, Handler(Looper.getMainLooper()))
|
||||
|
||||
// Get the daily total step count of a specified day
|
||||
fun requestDailyStepCount(startTime: Long) {
|
||||
if (startTime >= TODAY_START_UTC_TIME) {
|
||||
// Get today step count
|
||||
readStepCount(startTime)
|
||||
} else {
|
||||
// Get historical step count
|
||||
readStepDailyTrend(startTime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readStepCount(startTime: Long) {
|
||||
// Get sum of step counts by device
|
||||
val request = AggregateRequest.Builder()
|
||||
.setDataType(StepCount.HEALTH_DATA_TYPE)
|
||||
.addFunction(AggregateFunction.SUM, StepCount.COUNT, ALIAS_TOTAL_COUNT)
|
||||
.addGroup(StepCount.DEVICE_UUID, ALIAS_DEVICE_UUID)
|
||||
.setLocalTimeRange(StepCount.START_TIME, StepCount.TIME_OFFSET, startTime, startTime + TIME_INTERVAL)
|
||||
.setSort(ALIAS_TOTAL_COUNT, SortOrder.DESC)
|
||||
.build()
|
||||
|
||||
runCatching { healthDataResolver.aggregate(request) }
|
||||
.onFailure { Log.e(TAG, "Getting step count fails.", it) }
|
||||
.getOrNull()
|
||||
?.setResultListener {
|
||||
it.use {
|
||||
it.firstOrNull()
|
||||
.also { observer.onChanged(it?.getInt(ALIAS_TOTAL_COUNT) ?: 0) }
|
||||
?.let { readStepCountBinning(startTime, it.getString(ALIAS_DEVICE_UUID)) }
|
||||
?: observer.onBinningDataChanged(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readStepDailyTrend(dayStartTime: Long) {
|
||||
val request = ReadRequest.Builder()
|
||||
.setDataType(StepDailyTrend.HEALTH_DATA_TYPE)
|
||||
.setProperties(arrayOf(StepDailyTrend.COUNT, StepDailyTrend.BINNING_DATA))
|
||||
.setFilter(Filter.and(
|
||||
Filter.eq(StepDailyTrend.DAY_TIME, dayStartTime),
|
||||
Filter.eq(StepDailyTrend.SOURCE_TYPE, StepDailyTrend.SOURCE_TYPE_ALL)))
|
||||
.build()
|
||||
|
||||
runCatching { healthDataResolver.read(request) }
|
||||
.onFailure { Log.e(TAG, "Getting daily step trend fails.", it) }
|
||||
.getOrNull()
|
||||
?.setResultListener {
|
||||
it.use {
|
||||
it.firstOrNull().also {
|
||||
observer.onChanged(it?.getInt(StepDailyTrend.COUNT) ?: 0)
|
||||
observer.onBinningDataChanged(
|
||||
it?.getBlob(StepDailyTrend.BINNING_DATA)?.let { getBinningData(it) } ?: emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBinningData(zip: ByteArray): List<StepBinningData> {
|
||||
// decompress ZIP
|
||||
val binningDataList = HealthDataUtil.getStructuredDataList(zip, StepBinningData::class.java)
|
||||
return binningDataList.asSequence()
|
||||
.withIndex()
|
||||
.filter { it.value.count != 0 }
|
||||
.onEach { it.value.time = String.format(Locale.US, "%02d:%02d", it.index / 6, it.index % 6 * 10) }
|
||||
.map { it.value }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun readStepCountBinning(startTime: Long, deviceUuid: String) {
|
||||
|
||||
// Get 10 minute binning data of a particular device
|
||||
val request = AggregateRequest.Builder()
|
||||
.setDataType(StepCount.HEALTH_DATA_TYPE)
|
||||
.addFunction(AggregateFunction.SUM, StepCount.COUNT, ALIAS_TOTAL_COUNT)
|
||||
.setTimeGroup(TimeGroupUnit.MINUTELY, 10, StepCount.START_TIME, StepCount.TIME_OFFSET, ALIAS_BINNING_TIME)
|
||||
.setLocalTimeRange(StepCount.START_TIME, StepCount.TIME_OFFSET, startTime, startTime + TIME_INTERVAL)
|
||||
.setFilter(Filter.eq(StepCount.DEVICE_UUID, deviceUuid))
|
||||
.setSort(ALIAS_BINNING_TIME, SortOrder.ASC)
|
||||
.build()
|
||||
|
||||
runCatching { healthDataResolver.aggregate(request) }
|
||||
.onFailure { Log.e(TAG, "Getting step binning data fails.", it) }
|
||||
.getOrNull()
|
||||
?.setResultListener {
|
||||
it.use {
|
||||
it.asSequence()
|
||||
.map { it.getString(ALIAS_BINNING_TIME) to it.getInt(ALIAS_TOTAL_COUNT) }
|
||||
.filter { it.first != null }
|
||||
.map { StepBinningData(it.first.split(" ")[1], it.second) }
|
||||
.toList()
|
||||
}
|
||||
.also { observer.onBinningDataChanged(it) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TODAY_START_UTC_TIME = todayStartUtcTime
|
||||
val TIME_INTERVAL = TimeUnit.DAYS.toMillis(1)
|
||||
private const val ALIAS_TOTAL_COUNT = "count"
|
||||
private const val ALIAS_DEVICE_UUID = "deviceuuid"
|
||||
private const val ALIAS_BINNING_TIME = "binning_time"
|
||||
const val TAG = "SamsungHealth.StepCountReader"
|
||||
|
||||
private val todayStartUtcTime: Long
|
||||
get() {
|
||||
val today = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
today[Calendar.HOUR_OF_DAY] = 0
|
||||
today[Calendar.MINUTE] = 0
|
||||
today[Calendar.SECOND] = 0
|
||||
today[Calendar.MILLISECOND] = 0
|
||||
return today.timeInMillis
|
||||
}
|
||||
}
|
||||
}
|
16
app/src/main/java/com/dzeio/openhealth/core/BaseDao.kt
Normal file
16
app/src/main/java/com/dzeio/openhealth/core/BaseDao.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package com.dzeio.openhealth.core
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import com.dzeio.openhealth.db.entities.Weight
|
||||
|
||||
interface BaseDao<T> {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg obj: T)
|
||||
|
||||
@Update
|
||||
fun update(vararg obj: T)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg obj: T)
|
||||
}
|
37
app/src/main/java/com/dzeio/openhealth/db/AppDatabase.kt
Normal file
37
app/src/main/java/com/dzeio/openhealth/db/AppDatabase.kt
Normal file
@ -0,0 +1,37 @@
|
||||
package com.dzeio.openhealth.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.dzeio.openhealth.db.dao.WeightDao
|
||||
import com.dzeio.openhealth.db.entities.Weight
|
||||
|
||||
@Database(entities = [Weight::class], version = 1, exportSchema = false)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun weightDao() : WeightDao
|
||||
|
||||
companion object {
|
||||
// For Singleton instantiation
|
||||
@Volatile
|
||||
private var INSTANCE: AppDatabase? = null
|
||||
private const val DATABASE_NAME = "open_health"
|
||||
|
||||
fun getInstance(context: Context): AppDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
var instance = INSTANCE
|
||||
if (instance == null) {
|
||||
instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java,
|
||||
DATABASE_NAME
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
app/src/main/java/com/dzeio/openhealth/db/dao/WeightDao.kt
Normal file
24
app/src/main/java/com/dzeio/openhealth/db/dao/WeightDao.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package com.dzeio.openhealth.db.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import androidx.room.OnConflictStrategy.REPLACE
|
||||
import com.dzeio.openhealth.core.BaseDao
|
||||
import com.dzeio.openhealth.db.entities.Weight
|
||||
|
||||
@Dao
|
||||
interface WeightDao : BaseDao<Weight> {
|
||||
|
||||
@Query("SELECT * FROM Weight")
|
||||
fun getAll(): List<Weight>
|
||||
|
||||
@Query("SELECT * FROM Weight where id = :weightId")
|
||||
fun getOne(weightId: Long): Weight?
|
||||
|
||||
|
||||
@Query("Select count(*) from Weight")
|
||||
fun getCount(): Int
|
||||
|
||||
@Query("Select * FROM Weight WHERE id=(SELECT max(id) FROM Weight)")
|
||||
fun last(): Weight?
|
||||
}
|
15
app/src/main/java/com/dzeio/openhealth/db/entities/Weight.kt
Normal file
15
app/src/main/java/com/dzeio/openhealth/db/entities/Weight.kt
Normal file
@ -0,0 +1,15 @@
|
||||
package com.dzeio.openhealth.db.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.sql.Timestamp
|
||||
|
||||
@Entity()
|
||||
data class Weight (
|
||||
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
||||
var weight: Float = 0f,
|
||||
@ColumnInfo(index = true)
|
||||
var timestamp: Long = System.currentTimeMillis(),
|
||||
var source: String = ""
|
||||
)
|
@ -13,7 +13,9 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.dzeio.openhealth.connectors.ActionRequestCode
|
||||
import com.dzeio.openhealth.connectors.GoogleFit
|
||||
import com.dzeio.openhealth.connectors.samsunghealth.SamsungHealth
|
||||
import com.dzeio.openhealth.databinding.FragmentHomeBinding
|
||||
import com.dzeio.openhealth.db.AppDatabase
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
@ -29,27 +31,48 @@ class HomeFragment : Fragment() {
|
||||
|
||||
private lateinit var fit: GoogleFit
|
||||
|
||||
private lateinit var viewModel: HomeViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val homeViewModel =
|
||||
viewModel =
|
||||
ViewModelProvider(this).get(HomeViewModel::class.java)
|
||||
|
||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
val root: View = binding.root
|
||||
|
||||
binding.button.setOnClickListener {
|
||||
fit = GoogleFit(requireActivity())
|
||||
fit.import()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
fit.getHistory()
|
||||
}
|
||||
// binding.button.setOnClickListener {
|
||||
// fit = GoogleFit(requireActivity())
|
||||
// //fit.import()
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// //fit.getHistory()
|
||||
// fit.importWeight()
|
||||
// }
|
||||
//
|
||||
// //SamsungHealth(requireActivity()).importStepCount()
|
||||
// }
|
||||
viewModel.text.observe(viewLifecycleOwner) {
|
||||
binding.textView2.text = it
|
||||
}
|
||||
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val weight = AppDatabase.getInstance(requireContext()).weightDao().last()
|
||||
if (weight == null) {
|
||||
viewModel.text.postValue("No Weight Available")
|
||||
} else {
|
||||
viewModel.text.postValue("${weight.weight}kg\ndone at ${weight.timestamp}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
@ -2,12 +2,13 @@ package com.dzeio.openhealth.ui.home
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
|
||||
private val _text = MutableLiveData<String>().apply {
|
||||
class HomeViewModel(
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
val text = MutableLiveData<String>().apply {
|
||||
value = "This is home Fragment"
|
||||
}
|
||||
val text: LiveData<String> = _text
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package com.dzeio.openhealth.ui.import
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.dzeio.openhealth.connectors.GoogleFit
|
||||
import com.dzeio.openhealth.databinding.FragmentImportBinding
|
||||
|
||||
class ImportFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentImportBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and
|
||||
// onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var viewModel: ImportViewModel
|
||||
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
|
||||
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
|
||||
|
||||
progressDialog = ProgressDialog(requireContext())
|
||||
|
||||
progressDialog.apply {
|
||||
setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
setCancelable(false)
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.importGoogleFit.setOnClickListener {
|
||||
importFromGoogleFit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun importFromGoogleFit() {
|
||||
viewModel.importProgressTotal.postValue(-1)
|
||||
val google = GoogleFit(requireActivity())
|
||||
|
||||
progressDialog.show()
|
||||
google.importWeight {
|
||||
progressDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun importFromSamsungHealth() {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.dzeio.openhealth.ui.import
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.dzeio.openhealth.connectors.GoogleFit
|
||||
|
||||
class ImportViewModel : ViewModel() {
|
||||
|
||||
val text = MutableLiveData<String>().apply {
|
||||
value = "This is slideshow Fragment"
|
||||
}
|
||||
val importProgress = MutableLiveData<Int>().apply {
|
||||
value = 0
|
||||
}
|
||||
// If -1 progress is undetermined
|
||||
// If 0 no progress bar
|
||||
// Else progress bar
|
||||
val importProgressTotal = MutableLiveData<Int>().apply {
|
||||
value = 0
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package com.dzeio.openhealth.ui.slideshow
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.dzeio.openhealth.databinding.FragmentSlideshowBinding
|
||||
|
||||
class SlideshowFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentSlideshowBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and
|
||||
// onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val slideshowViewModel =
|
||||
ViewModelProvider(this).get(SlideshowViewModel::class.java)
|
||||
|
||||
_binding = FragmentSlideshowBinding.inflate(inflater, container, false)
|
||||
val root: View = binding.root
|
||||
|
||||
val textView: TextView = binding.textSlideshow
|
||||
slideshowViewModel.text.observe(viewLifecycleOwner) {
|
||||
textView.text = it
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package com.dzeio.openhealth.ui.slideshow
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class SlideshowViewModel : ViewModel() {
|
||||
|
||||
private val _text = MutableLiveData<String>().apply {
|
||||
value = "This is slideshow Fragment"
|
||||
}
|
||||
val text: LiveData<String> = _text
|
||||
}
|
@ -6,11 +6,12 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.home.HomeFragment">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Button"
|
||||
tools:layout_editor_absoluteX="157dp"
|
||||
tools:layout_editor_absoluteY="341dp" />
|
||||
android:text="TextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
43
app/src/main/res/layout/fragment_import.xml
Normal file
43
app/src/main/res/layout/fragment_import.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.import.ImportFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_slideshow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.044" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/import_google_fit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Import From Google Fit"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text_slideshow" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/import_samsung_health"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Import From Samsung Health"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.501"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/import_google_fit" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.slideshow.SlideshowFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_slideshow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -13,8 +13,8 @@
|
||||
android:icon="@drawable/ic_menu_gallery"
|
||||
android:title="@string/menu_gallery" />
|
||||
<item
|
||||
android:id="@+id/nav_slideshow"
|
||||
android:id="@+id/nav_import"
|
||||
android:icon="@drawable/ic_menu_slideshow"
|
||||
android:title="@string/menu_slideshow" />
|
||||
android:title="@string/menu_import" />
|
||||
</group>
|
||||
</menu>
|
@ -18,8 +18,8 @@
|
||||
tools:layout="@layout/fragment_gallery" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_slideshow"
|
||||
android:name="com.dzeio.openhealth.ui.slideshow.SlideshowFragment"
|
||||
android:label="@string/menu_slideshow"
|
||||
tools:layout="@layout/fragment_slideshow" />
|
||||
android:id="@+id/nav_import"
|
||||
android:name="com.dzeio.openhealth.ui.import.ImportFragment"
|
||||
android:label="@string/menu_import"
|
||||
tools:layout="@layout/fragment_import" />
|
||||
</navigation>
|
@ -10,4 +10,5 @@
|
||||
<string name="menu_home">Home</string>
|
||||
<string name="menu_gallery">Gallery</string>
|
||||
<string name="menu_slideshow">Slideshow</string>
|
||||
<string name="menu_import">Import</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user