diff --git a/.idea/misc.xml b/.idea/misc.xml index 1c422d0..59e6591 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,6 +10,10 @@ + + + + diff --git a/app/build.gradle b/app/build.gradle index 012daca..801e482 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,9 @@ plugins { id 'org.jetbrains.kotlin.android' // chaquopy lib for running python code id 'com.chaquo.python' + id 'androidx.navigation.safeargs.kotlin' + id 'com.google.gms.google-services' + id "kotlin-parcelize" } android { @@ -44,6 +47,7 @@ android { buildFeatures{ dataBinding = true } + namespace 'com.devsoc.hrmaa' } dependencies { @@ -53,6 +57,8 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1' implementation 'androidx.navigation:navigation-ui-ktx:2.5.1' + implementation 'androidx.browser:browser:1.4.0' + implementation 'com.google.firebase:firebase-firestore-ktx:24.4.1' testImplementation 'junit:junit:4.13.2' implementation 'com.airbnb.android:lottie:5.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.3' @@ -71,4 +77,17 @@ dependencies { //Splash Screen implementation 'androidx.core:core-splashscreen:1.0.0' + + //retrofit + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + + //Gson + implementation 'com.squareup.retrofit2:converter-gson:2.1.0' + + //Http + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + + //Graph plotter + implementation 'com.jjoe64:graphview:4.2.2' + } \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..c2f8f70 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "557049753871", + "project_id": "hrmaa-fitbit", + "storage_bucket": "hrmaa-fitbit.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:557049753871:android:dd440e381f00815b5ace7a", + "android_client_info": { + "package_name": "com.devsoc.hrmaa" + } + }, + "oauth_client": [ + { + "client_id": "557049753871-31hlvd6d5m4rc314nm1aiq39ukh2u1lb.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDnJPVzxFEmzK6d-lA73IddXnysYifj0kc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "557049753871-31hlvd6d5m4rc314nm1aiq39ukh2u1lb.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30467ef..a05c297 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,11 +2,13 @@ - + + + @@ -24,12 +26,22 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.App.Starting" + android:theme="@style/Theme.HRMAA_PROTOTYPE" tools:targetApi="31"> + + + + android:theme="@style/Theme.HRMAA_PROTOTYPE.NoActionBar" + android:screenOrientation="portrait"> @@ -37,23 +49,52 @@ + android:theme="@style/Theme.HRMAA_PROTOTYPE.NoActionBar" + android:screenOrientation="portrait"/> + + + + + + + + + + android:theme="@style/Theme.HRMAA_PROTOTYPE.NoActionBar" + android:screenOrientation="portrait"/> + android:exported="true" + android:theme="@style/Theme.HRMAA_PROTOTYPE.NoActionBar"> + + + + + + + + + android:theme="@style/Theme.HRMAA_PROTOTYPE.NoActionBar" + android:screenOrientation="portrait"/> + android:exported="true" + android:theme="@style/Theme.HRMAA_PROTOTYPE.NoActionBar"> diff --git a/app/src/main/java/com/devsoc/hrmaa/MainActivity.kt b/app/src/main/java/com/devsoc/hrmaa/MainActivity.kt index 3abb51c..950e4af 100644 --- a/app/src/main/java/com/devsoc/hrmaa/MainActivity.kt +++ b/app/src/main/java/com/devsoc/hrmaa/MainActivity.kt @@ -3,10 +3,11 @@ package com.devsoc.hrmaa import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.databinding.DataBindingUtil -import com.devsoc.hrmaa.bluetooth.AvailableDevicesActivity +import androidx.health.connect.client.HealthConnectClient import com.devsoc.hrmaa.bluetooth.ECGHome import com.devsoc.hrmaa.databinding.ActivityMainBinding import com.devsoc.hrmaa.fitbit.FitbitActivity @@ -21,20 +22,29 @@ class MainActivity : AppCompatActivity() { installSplashScreen() binding = DataBindingUtil.setContentView(this, R.layout.activity_main) - - binding.hcCvMa.setOnClickListener{ - startActivity(Intent(this,HealthConnectActivity::class.java)) + binding.hcCvMa.setOnClickListener { + //launch only if Health Connect is installed on device + if (HealthConnectClient.isAvailable(this)) { + startActivity(Intent(this, HealthConnectActivity::class.java)) + } else { + Toast.makeText( + this, + "Please install the Google Health Connect App first.", + Toast.LENGTH_SHORT + ).show() + } } - binding.fitbitCvMa.setOnClickListener{ - startActivity(Intent(this,FitbitActivity::class.java)) + binding.fitbitCvMa.setOnClickListener { + startActivity(Intent(this, FitbitActivity::class.java)) } - binding.ppgCvMa.setOnClickListener{ + binding.ppgCvMa.setOnClickListener { startActivity(Intent(this, PPGActivity::class.java)) } binding.ecgCvMa.setOnClickListener{ - startActivity(Intent(this,ECGHome::class.java)) + startActivity(Intent(this, ECGHome::class.java)) } + } } \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/bluetooth/ECGHome.kt b/app/src/main/java/com/devsoc/hrmaa/bluetooth/ECGHome.kt index 2d4461e..83ee68e 100644 --- a/app/src/main/java/com/devsoc/hrmaa/bluetooth/ECGHome.kt +++ b/app/src/main/java/com/devsoc/hrmaa/bluetooth/ECGHome.kt @@ -237,6 +237,7 @@ class ECGHome : AppCompatActivity() { } apnaSocket?.close() bluetoothAdapter?.cancelDiscovery() + btDevice?.let { apnaSocket = it.createInsecureRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")) try { @@ -251,72 +252,77 @@ class ECGHome : AppCompatActivity() { Toast.makeText(this@ECGHome, e.message, Toast.LENGTH_SHORT).show() } } - } - - inStream =apnaSocket?.inputStream - outStream=apnaSocket?.outputStream - try{ - outStream?.write(0) - if(outStream==null){ - Log.d(TAG,"outStream is null") - } - } - catch (e: IOException) { - Log.e(TAG, "Error occurred when sending data", e) - } - - val reader = BufferedReader(InputStreamReader(inStream)) + inStream =apnaSocket?.inputStream + outStream=apnaSocket?.outputStream - var beforeLoopTime = System.currentTimeMillis() - while (true) { try{ - currStr = reader.readLine() - withContext(Dispatchers.Main) { - val compositeData = currStr.toLong() - ECGDataList.add((compositeData % 10000).toInt()) - timeStampList.add(compositeData / 10000) + outStream?.write(0) + if(outStream==null){ + Log.d(TAG,"outStream is null") } - } - catch(e: java.lang.NumberFormatException){ - } catch (e: IOException) { - Log.d(TAG, "Input stream was disconnected", e) - withContext(Dispatchers.Main){ - Toast.makeText(this@ECGHome,e.message, Toast.LENGTH_LONG).show() - } - break + Log.e(TAG, "Error occurred when sending data", e) } - if( (System.currentTimeMillis() - beforeLoopTime) > 20000 ){ - Log.d("HeartList",ECGDataList.toString()) - Log.d("TimeList",timeStampList.toString()) - - try { + val reader = BufferedReader(InputStreamReader(inStream)) + var beforeLoopTime = System.currentTimeMillis() + while (true) { + try{ + currStr = reader.readLine() withContext(Dispatchers.Main) { - val dataList = module.callAttr( - "get_bpm_metric", - ECGDataList.toIntArray(), - timeStampList.toLongArray() - ).asList() - binding.tvHeartRate.text = dataList.get(0).toString() - Log.d("Contents of List", dataList.toString()) - ECGDataList.clear() - timeStampList.clear() + val compositeData = currStr.toLong() + ECGDataList.add((compositeData % 10000).toInt()) + timeStampList.add(compositeData / 10000) } } - catch(e: PyException){ - withContext(Dispatchers.Main) { - Toast.makeText(this@ECGHome, e.message, Toast.LENGTH_SHORT).show() - Log.e("Error in python script",e.message + "\n" + e.cause + "\n" + e.toString()) - } + catch(e: java.lang.NumberFormatException){ + } - finally { - beforeLoopTime = System.currentTimeMillis() + catch (e: IOException) { + Log.d(TAG, "Input stream was disconnected", e) + withContext(Dispatchers.Main){ + Toast.makeText(this@ECGHome,e.message, Toast.LENGTH_LONG).show() + } + break } + if( (System.currentTimeMillis() - beforeLoopTime) > 20000 ){ + Log.d("HeartList",ECGDataList.toString()) + Log.d("TimeList",timeStampList.toString()) + + try { + + withContext(Dispatchers.Main) { + val dataList = module.callAttr( + "get_bpm_metric", + ECGDataList.toIntArray(), + timeStampList.toLongArray() + ).asList() + binding.tvHeartRate.text = dataList.get(0).toString() + Log.d("Contents of List", dataList.toString()) + ECGDataList.clear() + timeStampList.clear() + } + } + catch(e: PyException){ + withContext(Dispatchers.Main) { + Toast.makeText(this@ECGHome, e.message, Toast.LENGTH_SHORT).show() + Log.e("Error in python script",e.message + "\n" + e.cause + "\n" + e.toString()) + } + } + finally { + beforeLoopTime = System.currentTimeMillis() + } + + } + } + } + if( btDevice == null){ + withContext(Dispatchers.Main){ + Toast.makeText(this@ECGHome,"No Bluetooth Device Selected", Toast.LENGTH_SHORT).show() } } } diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/FitbitActivity.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/FitbitActivity.kt index f8e7898..bdd3a79 100644 --- a/app/src/main/java/com/devsoc/hrmaa/fitbit/FitbitActivity.kt +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/FitbitActivity.kt @@ -1,12 +1,27 @@ package com.devsoc.hrmaa.fitbit +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.navigation.findNavController import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.ActivityFitbitBinding class FitbitActivity : AppCompatActivity() { + + private lateinit var binding: ActivityFitbitBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_fitbit) + binding = DataBindingUtil.setContentView(this, R.layout.activity_fitbit) + val view = binding.root + setContentView(view) } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + findNavController(R.id.fitbit_nav_graph).handleDeepLink(intent) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/EcgAdapter.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/EcgAdapter.kt new file mode 100644 index 0000000..c59fccf --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/EcgAdapter.kt @@ -0,0 +1,37 @@ +package com.devsoc.hrmaa.fitbit.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.fitbit.dataclasses.ActivitiesHeart +import com.devsoc.hrmaa.fitbit.dataclasses.EcgReading + +class EcgAdapter(val ecgSeries: List): RecyclerView.Adapter() { + + var onItemClick : ((EcgReading) -> Unit)? = null + inner class EcgViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EcgViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.heart_rate_log, parent, false) + return EcgViewHolder(view) + } + + override fun onBindViewHolder(holder: EcgViewHolder, position: Int) { + val startTime = ecgSeries[position].startTime + holder.itemView.apply { + findViewById(R.id.date_tv_hrl).text = startTime.substring(0,10)+" "+startTime.substring(11) + findViewById(R.id.heart_rate_tv_hrl).text = "${ecgSeries[position].averageHeartRate} BPM" + + setOnClickListener { + onItemClick?.invoke(ecgSeries[position]) + } + } + } + + override fun getItemCount(): Int { + return ecgSeries.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/HeartRateAdapter.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/HeartRateAdapter.kt new file mode 100644 index 0000000..6e9206d --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/HeartRateAdapter.kt @@ -0,0 +1,39 @@ +package com.devsoc.hrmaa.fitbit.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.fitbit.dataclasses.ActivitiesHeart +import com.devsoc.hrmaa.fitbit.dataclasses.HeartRateSeries +import com.devsoc.hrmaa.fitbit.dataclasses.HeartRateZone + +class HeartRateAdapter(val heartRateSeries: List): RecyclerView.Adapter() { + var onItemClick : ((ActivitiesHeart) -> Unit)? = null + + inner class HeartRateViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeartRateViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.heart_rate_log, parent, false) + return HeartRateViewHolder(view) + } + + override fun onBindViewHolder(holder: HeartRateViewHolder, position: Int) { + holder.itemView.apply { + findViewById(R.id.date_tv_hrl).text = heartRateSeries[position].dateTime + findViewById(R.id.heart_rate_tv_hrl).text = "${heartRateSeries[position].value.restingHeartRate} BPM" + + setOnClickListener { + onItemClick?.invoke(heartRateSeries[position]) + } + } + } + + override fun getItemCount(): Int { + return heartRateSeries.size + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/HeartRateZoneAdapter.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/HeartRateZoneAdapter.kt new file mode 100644 index 0000000..9244aa7 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/adapters/HeartRateZoneAdapter.kt @@ -0,0 +1,33 @@ +package com.devsoc.hrmaa.fitbit.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.fitbit.dataclasses.HeartRateZone + +class HeartRateZoneAdapter(val heartRateZones: List): RecyclerView.Adapter() { + + inner class HeartRateZoneViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeartRateZoneViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.heart_rate_zone, parent, false) + return HeartRateZoneViewHolder(view) + } + + override fun onBindViewHolder(holder: HeartRateZoneViewHolder, position: Int) { + holder.itemView.apply { + findViewById(R.id.zone_name_tv).text = "Zone Name: ${heartRateZones[position].name}" + findViewById(R.id.max_tv).text = "Max: ${heartRateZones[position].max}" + findViewById(R.id.min_tv).text = "Min: ${heartRateZones[position].min}" + findViewById(R.id.time_tv).text = "Minutes: ${heartRateZones[position].minutes}" + } + } + + override fun getItemCount(): Int { + return heartRateZones.size + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/ActivitiesHeart.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/ActivitiesHeart.kt new file mode 100644 index 0000000..61195ba --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/ActivitiesHeart.kt @@ -0,0 +1,10 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ActivitiesHeart( + val dateTime: String?, + val value: Value +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/AuthInfo.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/AuthInfo.kt new file mode 100644 index 0000000..50ca8bd --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/AuthInfo.kt @@ -0,0 +1,10 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +import com.google.gson.annotations.SerializedName + +data class AuthInfo( + @SerializedName("clientId") val clientId: String, + @SerializedName("grant_type") val grant_type: String, + @SerializedName("redirect_uri") val redirect_uri: String, + @SerializedName("code") val code: String +) diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/CustomHeartRateZone.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/CustomHeartRateZone.kt new file mode 100644 index 0000000..dbcd5cb --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/CustomHeartRateZone.kt @@ -0,0 +1,13 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class CustomHeartRateZone( + val caloriesOut: Double, + val max: Int, + val min: Int, + val minutes: Int, + val name: String +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/EcgData.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/EcgData.kt new file mode 100644 index 0000000..cac49d4 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/EcgData.kt @@ -0,0 +1,6 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +data class EcgData( + val ecgReadings: List, + val pagination : Pagination +) diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/EcgReading.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/EcgReading.kt new file mode 100644 index 0000000..86bad47 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/EcgReading.kt @@ -0,0 +1,15 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +data class EcgReading( + val averageHeartRate: Int, + val deviceName: String, + val featureVersion: String, + val firmwareVersion: String, + val leadNumber: Int, + val numberOfWaveformSample: Int, + val resultClassification: String, + val samplingFrequencyHz: String, + val scalingFactor: Int, + val startTime: String, + val waveformSamples: List +) diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/HeartRateSeries.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/HeartRateSeries.kt new file mode 100644 index 0000000..bed23bb --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/HeartRateSeries.kt @@ -0,0 +1,5 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +data class HeartRateSeries( + val activities_heart: List +) \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/HeartRateZone.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/HeartRateZone.kt new file mode 100644 index 0000000..d12d845 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/HeartRateZone.kt @@ -0,0 +1,13 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class HeartRateZone( + val caloriesOut: Double, + val max: Int, + val min: Int, + val minutes: Int, + val name: String +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/Pagination.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/Pagination.kt new file mode 100644 index 0000000..de44a5d --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/Pagination.kt @@ -0,0 +1,10 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +data class Pagination( + val afterDate : String, + val limit : Int, + val next : String, + val offset : Int, + val previous : String, + val sort : String +) diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/TokenData.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/TokenData.kt new file mode 100644 index 0000000..ef3bf2c --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/TokenData.kt @@ -0,0 +1,12 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +import com.google.gson.annotations.SerializedName + +data class TokenData( + @SerializedName("access_token") val access_token: String, + @SerializedName("expires_in") val expires_in: Int, + @SerializedName("refresh_token") val refresh_token: String, + @SerializedName("scope") val scope: String, + @SerializedName("token_type") val token_type: String, + @SerializedName("user_id") val user_id: String +) diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/Value.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/Value.kt new file mode 100644 index 0000000..4290e36 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/dataclasses/Value.kt @@ -0,0 +1,11 @@ +package com.devsoc.hrmaa.fitbit.dataclasses + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Value( + val customHeartRateZones: List, + val heartRateZones: List, + val restingHeartRate: Int +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/EcgDataFragment.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/EcgDataFragment.kt new file mode 100644 index 0000000..600cb88 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/EcgDataFragment.kt @@ -0,0 +1,187 @@ +package com.devsoc.hrmaa.fitbit.fragments + +import android.content.Context +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.FragmentEcgDataBinding +import com.devsoc.hrmaa.fitbit.adapters.EcgAdapter +import com.devsoc.hrmaa.fitbit.dataclasses.* +import com.devsoc.hrmaa.fitbit.interfaces.RestApi +import com.devsoc.hrmaa.fitbit.objects.ServiceBuilder +import com.google.firebase.firestore.FirebaseFirestore +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* + +class EcgDataFragment : Fragment() { + private lateinit var binding: FragmentEcgDataBinding + private val clientId: String = "238QCY" + private val redirectUri: String = "hrmaa://www.example.com/getCode" + private val fStore = FirebaseFirestore.getInstance() + private val cRef = fStore.collection("oauth") + private val dRef = cRef.document("test") + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + // Inflate the layout for this fragment + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_ecg_data, container, false) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val sharedPreference = + activity?.getSharedPreferences("PREFERENCE_NAME", Context.MODE_PRIVATE) + val code: String = sharedPreference?.getString("userId", null)!! + val authInfo = AuthInfo(clientId, "authorization_code", redirectUri, code) + + dRef.get().addOnCompleteListener {task -> + if(task.isSuccessful){ + val doc = task.result + if(doc.exists()){ + dRef.addSnapshotListener { value, _ -> + val time = value!!.getLong("date")!! + //check if access token has expired and refresh if expired + if (Date().time - time < 28800000) { + val accToken = value.getString("access_token")!! + getEcgInfo(accToken) + } else { + val refToken = value.getString("refresh_token")!! + refresh(refToken, authInfo) + } + } + } + else { + getTokenInfo(authInfo) + } + } + } + + } + + private fun getTokenInfo(authInfo: AuthInfo) { + val retrofit = ServiceBuilder.buildService(RestApi::class.java) + retrofit.getTokenInfo( + authInfo.clientId, + authInfo.grant_type, + authInfo.redirect_uri, + authInfo.code + ).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.d("Service", t.message + "") + } + + override fun onResponse(call: Call, response: Response) { + val tokenData = response.body() + Log.d("Access Response Code", "$response") + if (tokenData != null && response.raw().code == 200) { + val accessToken = tokenData.access_token + val refreshToken = tokenData.refresh_token + val uid = tokenData.user_id + val timestamp = hashMapOf( + "date" to Date().time, + "access_token" to accessToken, + "refresh_token" to refreshToken, + "uid" to uid + ) + dRef.set(timestamp) + + getEcgInfo(accessToken) + } + } + } + ) + } + + private fun refresh(refreshToken: String, authInfo: AuthInfo) { + val retrofit = ServiceBuilder.buildService(RestApi::class.java) + retrofit.refresh( + authInfo.clientId, + "refresh_token", + authInfo.redirect_uri, + refreshToken + ).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.d("Service", t.message.toString()) + } + + override fun onResponse(call: Call, response: Response) { + val tokenData = response.body() + Log.d("Refresh", "${response.raw().code}") + if (tokenData != null) { + val accessToken = tokenData.access_token + val newRefreshToken = tokenData.refresh_token + val uid = tokenData.user_id + val timestamp = hashMapOf( + "date" to Date().time, + "access_token" to accessToken, + "refresh_token" to newRefreshToken, + "uid" to uid + ) + dRef.set(timestamp) + getEcgInfo(accessToken) + } + } + } + ) + } + + fun getEcgInfo(accessToken: String) { + val headerMap = mutableMapOf() + headerMap["authorization"] = "Bearer $accessToken" + + val retrofit = ServiceBuilder.buildService(RestApi::class.java) + retrofit.getEcgData(headerMap).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.d("Service", t.message.toString()) + } + + override fun onResponse(call: Call, response: Response) { + val ecgData = response.body() + Log.d("ECG Response Code", "${response.raw().code}") + if (response.raw().code == 200 && ecgData != null) { + val ecgReadings = ecgData.ecgReadings + if(ecgReadings.isEmpty()){ + binding.dataTvEdf.text = "" + val ecg = mutableListOf( + EcgReading(69, "XYZ", "","", 1, 100, "","",10000, "2023-01-31 10pm", + mutableListOf(0)) + ) + val adapter = EcgAdapter(ecg) + binding.ecgRvEdfg.apply { + this.adapter = adapter + layoutManager = LinearLayoutManager(context) + } + adapter.onItemClick = { + findNavController().navigate(R.id.action_fitbitDataFragment_to_ecgGraphFragment) + } + } + else { + binding.dataTvEdf.text = "No ECG Data found!" + } + } else { + Log.d("ECG Response", response.raw().message) + } + } + } + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/EcgGraphFragment.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/EcgGraphFragment.kt new file mode 100644 index 0000000..5b49861 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/EcgGraphFragment.kt @@ -0,0 +1,61 @@ +package com.devsoc.hrmaa.fitbit.fragments + +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.provider.ContactsContract.Data +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.FragmentEcgGraphBinding +import com.jjoe64.graphview.series.DataPoint +import com.jjoe64.graphview.series.LineGraphSeries + +class EcgGraphFragment : Fragment() { + private lateinit var binding: FragmentEcgGraphBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_ecg_graph, container, false) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val graphView = binding.ecgGvEgf + + val voltages = intArrayOf(272,286,314,289,289,296,658,184,305,324,344,402,455,467,339,274,270,277,281,288,290,305,298,305,328,312,316,292,662,345,328,352,388,431,463,407,287,263,259,268,269,281,285,283,299,316,299,301,299,664,145,307,323,360,403,457,455,320,257,252,261,269,276,282,294,280,297,316,308,288,297,628,218,322,340,361,401,462,480,348,263,260,275,273,283,291,286,290,301,327,296,293,290,665,256,325,338,374,423,479,445,313,261 + ) + val times = intArrayOf(12800,12832,12865,12897,12930,12962,12995,13028,13060,13092,13125,13158,13190,13222,13255,13288,13321,13352,13385,13418,13451,13483,13515,13548,13581,13614,13645,13678,13711,13744,13775,13808,13841,13874,13906,13938,13971,14004,14036,14068,14101,14134,14167,14199,14231,14264,14297,14329,14361,14394,14427,14459,14492,14524,14557,14589,14622,14654,14687,14720,14752,14785,14817,14850,14882,14915,14947,14980,15012,15045,15078,15110,15142,15175,15208,15240,15272,15305,15338,15371,15403,15435,15468,15501,15533,15565,15598,15631,15664,15695,15728,15761,15794,15826,15858,15891,15924,15956,15988,16021,16054,16087,16119 + ) + + val dataPoints = mutableListOf() + for(i in 0..voltages.size-1){ + dataPoints.add(DataPoint((times[i]-times[0]).toDouble(), voltages[i].toDouble())) + } + + val series: LineGraphSeries = LineGraphSeries(dataPoints.toTypedArray()) + + graphView.animate() + graphView.viewport.isScrollable = true + graphView.viewport.isScalable = true + graphView.viewport.setScalableY(true) + graphView.viewport.setScrollableY(true) + graphView.viewport.setMaxX((times[times.size-1]-times[0]).toDouble()) + series.color = R.color.black + series.isDrawDataPoints = true + series.dataPointsRadius = 10F + series.setOnDataPointTapListener { series, dataPoint -> + binding.coordsTvEgf.text = "x = ${dataPoint.x} y = ${dataPoint.y}" + } + graphView.addSeries(series) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/FitbitAuthFragment.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/FitbitAuthFragment.kt new file mode 100644 index 0000000..7334b42 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/FitbitAuthFragment.kt @@ -0,0 +1,38 @@ +package com.devsoc.hrmaa.fitbit.fragments + +import android.content.pm.ActivityInfo +import android.net.Uri +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.browser.customtabs.CustomTabsIntent +import androidx.databinding.DataBindingUtil +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.FragmentFitbitAuthBinding + +class FitbitAuthFragment : Fragment() { + + private lateinit var binding: FragmentFitbitAuthBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_fitbit_auth, container, false) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.authBtBvFaf.setOnClickListener { + val url = "https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=238QCY&scope=activity+cardio_fitness+electrocardiogram+heartrate+location+nutrition+oxygen_saturation+profile+respiratory_rate+settings+sleep+social+temperature+weight" + val builder : CustomTabsIntent.Builder = CustomTabsIntent.Builder() + val customTabsIntent = builder.build() + customTabsIntent.launchUrl(requireContext(), Uri.parse(url)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/FitbitRedirectFragment.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/FitbitRedirectFragment.kt new file mode 100644 index 0000000..e37374b --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/FitbitRedirectFragment.kt @@ -0,0 +1,73 @@ +package com.devsoc.hrmaa.fitbit.fragments + +import android.content.Context +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.FragmentFitbitRedirectBinding +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase +import java.util.Date + + +class FitbitRedirectFragment : Fragment() { + + private lateinit var binding: FragmentFitbitRedirectBinding + private val args: FitbitRedirectFragmentArgs by navArgs() + + override fun onCreate(savedInstanceState: Bundle?) { + val sharedPreference = activity?.getSharedPreferences("PREFERENCE_NAME", Context.MODE_PRIVATE) + if(args.code != "no_code_found"){ + val editor = sharedPreference?.edit() + editor?.putString("userId",args.code) + editor?.commit() + editor?.apply() + } + val userId : String? = sharedPreference?.getString("userId", null) + if(userId == null){ + val navAction = FitbitRedirectFragmentDirections.actionFitbitRedirectFragmentToFitbitAuthFragment() + findNavController().navigate(navAction) + } + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_fitbit_redirect, container, false) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.LoginBvFrf.setOnClickListener { + val navAction = FitbitRedirectFragmentDirections.actionFitbitRedirectFragmentToFitbitAuthFragment() + view.findNavController().navigate(navAction) + } + val sharedPreference = activity?.getSharedPreferences("PREFERENCE_NAME", Context.MODE_PRIVATE) + val userId : String = sharedPreference?.getString("userId", null)!! + Log.d("Auth Code", userId) + + binding.ecgCvFrf.setOnClickListener { + view.findNavController().navigate(R.id.action_fitbitRedirectFragment_to_fitbitDataFragment) + } + + binding.bpmCvFrf.setOnClickListener { + view.findNavController().navigate(R.id.action_fitbitRedirectFragment_to_heartRateFragment) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/HeartRateFragment.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/HeartRateFragment.kt new file mode 100644 index 0000000..1a973b4 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/HeartRateFragment.kt @@ -0,0 +1,199 @@ +package com.devsoc.hrmaa.fitbit.fragments + +import android.content.Context +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.FragmentHeartRateBinding +import com.devsoc.hrmaa.fitbit.adapters.HeartRateAdapter +import com.devsoc.hrmaa.fitbit.dataclasses.* +import com.devsoc.hrmaa.fitbit.interfaces.RestApi +import com.devsoc.hrmaa.fitbit.objects.ServiceBuilder +import com.google.firebase.firestore.FirebaseFirestore +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.* + +class HeartRateFragment : Fragment() { + private lateinit var binding: FragmentHeartRateBinding + private val clientId: String = "238QCY" + private val redirectUri: String = "hrmaa://www.example.com/getCode" + private val fStore = FirebaseFirestore.getInstance() + private val cRef = fStore.collection("oauth") + private val dRef = cRef.document("test") + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_heart_rate, container, false) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val sharedPreference = + activity?.getSharedPreferences("PREFERENCE_NAME", Context.MODE_PRIVATE) + val code: String = sharedPreference?.getString("userId", null)!! + val authInfo = AuthInfo(clientId, "authorization_code", redirectUri, code) + + dRef.get().addOnCompleteListener {task -> + if(task.isSuccessful){ + val doc = task.result + if(doc.exists()){ + dRef.addSnapshotListener { value, _ -> + val time = value!!.getLong("date")!! + //check if access token has expired and refresh if expired + if (Date().time - time < 28800000) { + val accToken = value.getString("access_token")!! + getHeartRateSeries(accToken, "2019-01-01", "2020-01-01") + } else { + val refToken = value.getString("refresh_token")!! + refresh(refToken, authInfo) + } + } + } + else { + getTokenInfo(authInfo) + } + } + } + } + + private fun getTokenInfo(authInfo: AuthInfo) { + val retrofit = ServiceBuilder.buildService(RestApi::class.java) + retrofit.getTokenInfo( + authInfo.clientId, + authInfo.grant_type, + authInfo.redirect_uri, + authInfo.code + ).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.d("Service", t.message + "") + } + + override fun onResponse(call: Call, response: Response) { + val tokenData = response.body() + Log.d("Access Response Code", "$response") + if (tokenData != null && response.raw().code == 200) { + val accessToken = tokenData.access_token + val refreshToken = tokenData.refresh_token + val uid = tokenData.user_id + val timestamp = hashMapOf( + "date" to Date().time, + "access_token" to accessToken, + "refresh_token" to refreshToken, + "uid" to uid + ) + dRef.set(timestamp) + getHeartRateSeries(accessToken, "2019-01-01", "2020-01-01") + } + } + } + ) + } + + private fun refresh(refreshToken: String, authInfo: AuthInfo) { + val retrofit = ServiceBuilder.buildService(RestApi::class.java) + retrofit.refresh( + authInfo.clientId, + "refresh_token", + authInfo.redirect_uri, + refreshToken + ).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.d("Service", t.message + "") + } + + override fun onResponse(call: Call, response: Response) { + val tokenData = response.body() + Log.d("Refresh", "${response.code()}") + if (tokenData != null) { + val accessToken = tokenData.access_token + val newRefreshToken = tokenData.refresh_token + val uid = tokenData.user_id + val timestamp = hashMapOf( + "date" to Date().time, + "access_token" to accessToken, + "refresh_token" to newRefreshToken, + "uid" to uid + ) + dRef.set(timestamp) + getHeartRateSeries(accessToken, "2019-01-01", "2020-01-01") + } + } + } + ) + } + + private fun getHeartRateSeries(accessToken: String, startDate: String, endDate: String){ + val headerMap = mutableMapOf() + headerMap["authorization"] = "Bearer $accessToken" + + val retrofit = ServiceBuilder.buildService(RestApi::class.java) + retrofit.getHeartRateSeries(headerMap, startDate, endDate).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + Log.d("Service", t.message + "") + } + + override fun onResponse(call: Call, response: Response) { + val heartRateData = response.body() + Log.d("Heart Rate Response Code", "".plus(response.raw().code)) + if (response.raw().code == 200 && heartRateData != null) { + val activitiesHeart = heartRateData.activities_heart + if(activitiesHeart == null) { + val zones1 = mutableListOf( + HeartRateZone(1100.0, 120, 90, 30, "Test1"), + HeartRateZone(900.0, 90, 60, 15, "Test2") + ) + val zones2 = mutableListOf( + CustomHeartRateZone(1100.0, 120, 90, 30, "Custom1"), + CustomHeartRateZone(1100.0, 120, 90, 30, "Custom2") + ) + val activities = mutableListOf( + ActivitiesHeart("2023-01-31", Value(zones2, zones1, 72)), + ActivitiesHeart("2023-01-30", Value(zones2, zones1, 85)), + ActivitiesHeart("2023-01-29", Value(zones2, zones1, 65)), + ActivitiesHeart("2023-01-28", Value(zones2, zones1, 70)) + ) + val adapter = HeartRateAdapter(activities) + binding.noDataTvHrf.visibility = View.INVISIBLE + binding.heartRateRvHrf.apply { + visibility = View.VISIBLE + this.adapter = adapter + layoutManager = LinearLayoutManager(context) + } + adapter.onItemClick = { + val action = HeartRateFragmentDirections.actionHeartRateFragmentToHeartRateZonesFragment(it) + findNavController().navigate(action) + } + } + else { + binding.heartRateRvHrf.visibility = View.INVISIBLE + binding.noDataTvHrf.visibility = View.VISIBLE + } + } else { + binding.heartRateRvHrf.visibility = View.INVISIBLE + binding.noDataTvHrf.visibility = View.VISIBLE + Log.d("Heart Rate Response", response.raw().message) + } + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/HeartRateZonesFragment.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/HeartRateZonesFragment.kt new file mode 100644 index 0000000..7660590 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/fragments/HeartRateZonesFragment.kt @@ -0,0 +1,47 @@ +package com.devsoc.hrmaa.fitbit.fragments + +import android.content.pm.ActivityInfo +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.FragmentHeartRateZonesBinding +import com.devsoc.hrmaa.fitbit.adapters.HeartRateZoneAdapter +import com.devsoc.hrmaa.fitbit.dataclasses.ActivitiesHeart +import com.devsoc.hrmaa.fitbit.dataclasses.HeartRateZone + +class HeartRateZonesFragment : Fragment() { + private lateinit var binding: FragmentHeartRateZonesBinding + private val args: HeartRateZonesFragmentArgs by navArgs() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_heart_rate_zones, container, false) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val activities = args.activities + val customZones = mutableListOf() + for(c in activities.value.customHeartRateZones){ + customZones.add(HeartRateZone(c.caloriesOut, c.max, c.min, c.minutes, c.name)) + } + + val zones = activities.value.heartRateZones.plus(customZones) + val adapter = HeartRateZoneAdapter(zones) + binding.heartRateZonesRvHrzf.apply { + this.adapter = adapter + layoutManager = LinearLayoutManager(requireContext()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/interfaces/RestApi.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/interfaces/RestApi.kt new file mode 100644 index 0000000..1c32369 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/interfaces/RestApi.kt @@ -0,0 +1,48 @@ +package com.devsoc.hrmaa.fitbit.interfaces + +import com.devsoc.hrmaa.fitbit.dataclasses.EcgData +import com.devsoc.hrmaa.fitbit.dataclasses.HeartRateSeries +import com.devsoc.hrmaa.fitbit.dataclasses.TokenData +import retrofit2.Call +import retrofit2.http.* + +interface RestApi { + + @Headers("Authorization: Basic MjM4UUNZOmVjNWUyZGUwYTg1YmEzNjdlYWM1NzFhNTg3MWU4MGE5") + @POST("oauth2/token") + @FormUrlEncoded + fun getTokenInfo( + @Field("clientId") clientId: String, + @Field("grant_type") grant_type: String, + @Field("redirect_uri") redirect_uri: String, + @Field("code") code: String + ): Call + + @Headers("Authorization: Basic MjM4UUNZOmVjNWUyZGUwYTg1YmEzNjdlYWM1NzFhNTg3MWU4MGE5") + @POST("oauth2/token") + @FormUrlEncoded + fun refresh( + @Field("clientId") clientId: String, + @Field("grant_type") grant_type: String, + @Field("redirect_uri") redirect_uri: String, + @Field("refresh_token") refresh_token: String + ): Call + + @Headers("accept: application/json") + @GET("1/user/-/ecg/list.json?afterDate=2022-09-28&sort=asc&limit=1&offset=0") + fun getEcgData(@HeaderMap headers: Map): Call + + @Headers("accept: application/json") + @GET("1/user/-/activities/heart/date/{startDate}/{endDate}.json") + fun getHeartRateSeries(@HeaderMap headers: Map, + @Path(value = "startDate", encoded = true) startDate: String, + @Path(value = "endDate", encoded = true) endDate: String + ): Call + + @Headers("accept: application/json") + @GET("{path}") + fun nextPage(@HeaderMap headers: Map, + @Path(value = "path", encoded = true) path: String + ): Call + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/fitbit/objects/ServiceBuilder.kt b/app/src/main/java/com/devsoc/hrmaa/fitbit/objects/ServiceBuilder.kt new file mode 100644 index 0000000..722a960 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/fitbit/objects/ServiceBuilder.kt @@ -0,0 +1,21 @@ +package com.devsoc.hrmaa.fitbit.objects + +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object ServiceBuilder { + + private val httpClient = OkHttpClient.Builder() + private val client = httpClient.build() + + private val retrofit = Retrofit.Builder() + .baseUrl("https://api.fitbit.com/") + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build() + + fun buildService(service: Class): T{ + return retrofit.create(service) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/healthConnect/DateActivity.kt b/app/src/main/java/com/devsoc/hrmaa/healthConnect/DateActivity.kt new file mode 100644 index 0000000..761b136 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/healthConnect/DateActivity.kt @@ -0,0 +1,63 @@ +package com.devsoc.hrmaa.healthConnect + +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.widget.Toast +import androidx.core.text.isDigitsOnly +import com.devsoc.hrmaa.databinding.ActivityDateBinding + +class DateActivity : AppCompatActivity() { + private lateinit var binding: ActivityDateBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityDateBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + binding.getRecCvDa.setOnClickListener { + val startDate = binding.startDateTietDa.text.toString() + val endDate = binding.endDateTietDa.text.toString() + var startDay = 1 + var startMonth = 1 + var startYear = 2023 + var endDay = 1 + var endMonth = 1 + var endYear = 2023 + if(startDate!!.isNotEmpty() && startDate.substring(0,2).isDigitsOnly() && startDate.substring(3,5).isDigitsOnly() && startDate.substring(6,10).isDigitsOnly() + && endDate!!.isNotEmpty() && endDate.substring(0,2).isDigitsOnly() && endDate.substring(3,5).isDigitsOnly() && endDate.substring(6,10).isDigitsOnly() + && ((startDate[2] == '/' && startDate[5] == '/' && endDate[2] == '/' && endDate[5] == '/') || (startDate[2] == '-' && startDate[5] == '-' && endDate[2] == '-' && endDate[5] == '-'))){ + + startDay = startDate.substring(0,2).toInt() + startMonth = startDate.substring(3,5).toInt() + startYear = startDate.substring(6,10).toInt() + endDay = endDate.substring(0,2).toInt() + endMonth = endDate.substring(3,5).toInt() + endYear = endDate.substring(6,10).toInt() + if(startDay in 1..31 && startMonth in 1..12 && startYear in 1990..2023 + && endDay in 1..31 && endMonth in 1..12 && endYear in 1990..2023 + && ((endYear==startYear && endMonth==startMonth && endDay>startDay)||(endYear==startYear && endMonth>startMonth)|| endYear>startYear)){ + val intent = Intent(this@DateActivity, ReadDataActivity::class.java) + intent.putExtra("start_date", startDate) + intent.putExtra("end_date", endDate) + startActivity(intent) + + } + else { + Toast.makeText( + this@DateActivity, + "Enter a valid date!", + Toast.LENGTH_SHORT + ).show() + } + } + else { + Toast.makeText( + this@DateActivity, + "Enter a valid date!", + Toast.LENGTH_SHORT + ).show() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/healthConnect/HealthConnectActivity.kt b/app/src/main/java/com/devsoc/hrmaa/healthConnect/HealthConnectActivity.kt index 57104bc..7b3d82b 100644 --- a/app/src/main/java/com/devsoc/hrmaa/healthConnect/HealthConnectActivity.kt +++ b/app/src/main/java/com/devsoc/hrmaa/healthConnect/HealthConnectActivity.kt @@ -1,60 +1,86 @@ package com.devsoc.hrmaa.healthConnect +import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import androidx.activity.result.registerForActivityResult +import android.view.MotionEvent +import android.view.View +import androidx.core.content.ContextCompat import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.permission.Permission import androidx.health.connect.client.records.HeartRateRecord -import androidx.health.connect.client.records.StepsRecord import androidx.lifecycle.lifecycleScope import com.devsoc.hrmaa.R +import com.devsoc.hrmaa.databinding.ActivityHealthConnectBinding import kotlinx.coroutines.launch class HealthConnectActivity : AppCompatActivity() { + private lateinit var binding: ActivityHealthConnectBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_health_connect) + binding = ActivityHealthConnectBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) - if (HealthConnectClient.isAvailable(this)) { - // Health Connect is available and installed. - val healthConnectClient = HealthConnectClient.getOrCreate(this) + val healthConnectClient = HealthConnectClient.getOrCreate(this) - val requestPermissionActivityContract = healthConnectClient.permissionController.createRequestPermissionActivityContract() + val requestPermissionActivityContract = + healthConnectClient.permissionController.createRequestPermissionActivityContract() - val requestPermissions = - registerForActivityResult(requestPermissionActivityContract) { granted -> - if (granted.containsAll(PERMISSIONS)) { - // Permissions successfully granted - } else { - // Lack of required permissions + val requestPermissions = + registerForActivityResult(requestPermissionActivityContract) { + + } + // Create the permissions launcher. + + fun checkPermissionsAndRun(client: HealthConnectClient) { + lifecycleScope.launch { + val granted = client.permissionController.getGrantedPermissions(PERMISSIONS) + if (granted.containsAll(PERMISSIONS)) { + // Permissions already granted + + binding.readCvHca.setOnTouchListener(object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_DOWN -> binding.readCvHca.setCardBackgroundColor(ContextCompat.getColorStateList(applicationContext, com.google.android.material.R.color.material_grey_300)!!) + MotionEvent.ACTION_UP -> binding.readCvHca.setCardBackgroundColor(ContextCompat.getColorStateList(applicationContext, com.google.android.material.R.color.m3_ref_palette_white)!!) + } + + return v?.onTouchEvent(event) ?: true + } + }) + binding.readCvHca.setOnClickListener { + val intent = Intent(this@HealthConnectActivity, DateActivity::class.java) + startActivity(intent) } - } - // Create the permissions launcher. - - fun checkPermissionsAndRun(client: HealthConnectClient) { - lifecycleScope.launch { - val granted = client.permissionController.getGrantedPermissions(PERMISSIONS) - if (granted.containsAll(PERMISSIONS)) { - // Permissions already granted - } else { - requestPermissions.launch(PERMISSIONS) + + binding.manageCvHca.setOnTouchListener(object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_DOWN -> binding.manageCvHca.setCardBackgroundColor(ContextCompat.getColorStateList(applicationContext, com.google.android.material.R.color.material_grey_300)!!) + MotionEvent.ACTION_UP -> binding.manageCvHca.setCardBackgroundColor(ContextCompat.getColorStateList(applicationContext, com.google.android.material.R.color.m3_ref_palette_white)!!) + } + + return v?.onTouchEvent(event) ?: true + } + }) + binding.manageCvHca.setOnClickListener { + val launch = packageManager.getLaunchIntentForPackage("com.google.android.apps.healthdata") + startActivity(launch) } + + } else { + requestPermissions.launch(PERMISSIONS) } } - } else { - // ... } - - + checkPermissionsAndRun(healthConnectClient) } - val PERMISSIONS = + private val PERMISSIONS = setOf( Permission.createReadPermission(HeartRateRecord::class), Permission.createWritePermission(HeartRateRecord::class), - Permission.createReadPermission(StepsRecord::class), - Permission.createWritePermission(StepsRecord::class) ) } \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/healthConnect/HeartRateSeriesAdapter.kt b/app/src/main/java/com/devsoc/hrmaa/healthConnect/HeartRateSeriesAdapter.kt new file mode 100644 index 0000000..0079900 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/healthConnect/HeartRateSeriesAdapter.kt @@ -0,0 +1,35 @@ +package com.devsoc.hrmaa.healthConnect + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.health.connect.client.records.HeartRateRecord +import androidx.recyclerview.widget.RecyclerView +import com.devsoc.hrmaa.R +import org.w3c.dom.Text +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.* + +class HeartRateSeriesAdapter(val heartRateSeries: List): RecyclerView.Adapter() { + + inner class HeartRateSeriesViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeartRateSeriesViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.heart_rate_log_hc, parent, false) + return HeartRateSeriesViewHolder(view) + } + + override fun onBindViewHolder(holder: HeartRateSeriesViewHolder, position: Int) { + holder.itemView.apply { + findViewById(R.id.datetime_tv_hrlhc).text = SimpleDateFormat("dd-MM-yyyy hh:mm:ss a").format(Date.from(heartRateSeries[position].time)) + findViewById(R.id.bpm_tv_hrlhc).text = "${heartRateSeries[position].beatsPerMinute} BPM" + } + } + + override fun getItemCount(): Int { + return heartRateSeries.size + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/devsoc/hrmaa/healthConnect/ReadDataActivity.kt b/app/src/main/java/com/devsoc/hrmaa/healthConnect/ReadDataActivity.kt new file mode 100644 index 0000000..a3f1f48 --- /dev/null +++ b/app/src/main/java/com/devsoc/hrmaa/healthConnect/ReadDataActivity.kt @@ -0,0 +1,108 @@ +package com.devsoc.hrmaa.healthConnect + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.health.connect.client.HealthConnectClient +import androidx.health.connect.client.permission.Permission +import androidx.health.connect.client.records.HeartRateRecord +import androidx.health.connect.client.request.ReadRecordsRequest +import androidx.health.connect.client.time.TimeRangeFilter +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.devsoc.hrmaa.databinding.ActivityReadDataBinding +import kotlinx.coroutines.launch +import java.time.Instant +import java.time.ZonedDateTime +import java.util.* + +class ReadDataActivity : AppCompatActivity() { + private lateinit var binding: ActivityReadDataBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityReadDataBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + val startDate = intent.getStringExtra("start_date").toString() + val endDate = intent.getStringExtra("end_date").toString() + val sd = Date(startDate.substring(6,10).toInt(), startDate.substring(3,5).toInt(), startDate.substring(0,2).toInt()) + val ed = Date(endDate.substring(6,10).toInt(), endDate.substring(3,5).toInt(), endDate.substring(0,2).toInt()) + val healthConnectClient = HealthConnectClient.getOrCreate(this) + + val requestPermissionActivityContract = + healthConnectClient.permissionController.createRequestPermissionActivityContract() + + val requestPermissions = + registerForActivityResult(requestPermissionActivityContract) { + + } + // Create the permissions launcher. + + fun checkPermissionsAndRun(client: HealthConnectClient) { + lifecycleScope.launch { + val granted = client.permissionController.getGrantedPermissions(PERMISSIONS) + if (granted.containsAll(PERMISSIONS)) { + // Permissions already granted + val end = ed.toInstant() + val start = sd.toInstant() + readHeartRateByTimeRange(healthConnectClient, start, end) + + } else { + requestPermissions.launch(PERMISSIONS) + } + } + } + + checkPermissionsAndRun(healthConnectClient) + + } + + private fun readHeartRateByTimeRange( + healthConnectClient: HealthConnectClient, + startTime: Instant, + endTime: Instant + ) { + lifecycleScope.launch { + val response = + healthConnectClient.readRecords( + ReadRecordsRequest( + HeartRateRecord::class, + timeRangeFilter = TimeRangeFilter.between(startTime, endTime) + ) + ) + + val series = mutableListOf( + HeartRateRecord.Sample(Instant.now().minusSeconds(259200), 101), + HeartRateRecord.Sample(Instant.now().minusSeconds(172800), 89), + HeartRateRecord.Sample(Instant.now().minusSeconds(86400), 69), + HeartRateRecord.Sample(Instant.now(), 75) + ) + val adapter = HeartRateSeriesAdapter(series) + binding.heartRateSeriesRvRda.apply { + this.adapter = adapter + layoutManager = LinearLayoutManager(context) + } + + if (response.records.isNotEmpty()) { + binding.dataTvRdf.text = "" + /*for (rec in response.records) { + val adapter = HeartRateSeriesAdapter(rec.samples) + binding.heartRateSeriesRvRda.apply { + this.adapter = adapter + layoutManager = LinearLayoutManager(context) + } + }*/ + } else { + //binding.dataTvRdf.text = "No records found!" + } + + } + } + + private val PERMISSIONS = + setOf( + Permission.createReadPermission(HeartRateRecord::class), + Permission.createWritePermission(HeartRateRecord::class), + ) + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/hc_bg.png b/app/src/main/res/drawable/hc_bg.png new file mode 100644 index 0000000..37c8736 Binary files /dev/null and b/app/src/main/res/drawable/hc_bg.png differ diff --git a/app/src/main/res/drawable/health_connect_bg.png b/app/src/main/res/drawable/health_connect_bg.png new file mode 100644 index 0000000..5b5524a Binary files /dev/null and b/app/src/main/res/drawable/health_connect_bg.png differ diff --git a/app/src/main/res/drawable/heart_rate_icon.png b/app/src/main/res/drawable/heart_rate_icon.png new file mode 100644 index 0000000..33b7f03 Binary files /dev/null and b/app/src/main/res/drawable/heart_rate_icon.png differ diff --git a/app/src/main/res/drawable/settings_icon.png b/app/src/main/res/drawable/settings_icon.png new file mode 100644 index 0000000..2f3ece9 Binary files /dev/null and b/app/src/main/res/drawable/settings_icon.png differ diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 4a29529..e3c2271 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -11,41 +11,30 @@ + app:layout_constraintTop_toTopOf="parent"> - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" + app:srcCompat="@drawable/hrmaa_logo" /> diff --git a/app/src/main/res/layout/activity_available_devices.xml b/app/src/main/res/layout/activity_available_devices.xml index 2b958d6..056e23f 100644 --- a/app/src/main/res/layout/activity_available_devices.xml +++ b/app/src/main/res/layout/activity_available_devices.xml @@ -41,7 +41,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" + android:fontFamily="sans-serif" android:text="Available devices" + android:textColor="#000000" + android:textSize="25sp" + android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" @@ -51,8 +55,11 @@ android:id="@+id/tvPairedDevices" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="28dp" + android:layout_marginTop="40dp" android:text="Paired Devices" + android:textColor="#000000" + android:textSize="25sp" + android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.496" app:layout_constraintStart_toStartOf="parent" @@ -83,7 +90,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" + android:padding="10dp" android:text="discover" + android:textSize="20sp" + android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/activity_date.xml b/app/src/main/res/layout/activity_date.xml new file mode 100644 index 0000000..cfb13b4 --- /dev/null +++ b/app/src/main/res/layout/activity_date.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_ecghome.xml b/app/src/main/res/layout/activity_ecghome.xml index 8ef98cd..b21d8ea 100644 --- a/app/src/main/res/layout/activity_ecghome.xml +++ b/app/src/main/res/layout/activity_ecghome.xml @@ -70,9 +70,10 @@ - - - + xmlns:tools="http://schemas.android.com/tools"> - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_health_connect.xml b/app/src/main/res/layout/activity_health_connect.xml index a565303..e8e4da2 100644 --- a/app/src/main/res/layout/activity_health_connect.xml +++ b/app/src/main/res/layout/activity_health_connect.xml @@ -1,9 +1,168 @@ - + xmlns:tools="http://schemas.android.com/tools"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 73c03e6..998a40e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -10,10 +10,10 @@ - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" + app:srcCompat="@drawable/hrmaa_logo" /> diff --git a/app/src/main/res/layout/activity_read_data.xml b/app/src/main/res/layout/activity_read_data.xml new file mode 100644 index 0000000..4067cb7 --- /dev/null +++ b/app/src/main/res/layout/activity_read_data.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/device_card.xml b/app/src/main/res/layout/device_card.xml index 86c7aa7..f2f3efa 100644 --- a/app/src/main/res/layout/device_card.xml +++ b/app/src/main/res/layout/device_card.xml @@ -1,39 +1,58 @@ - - - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5"> - + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_ecg_data.xml b/app/src/main/res/layout/fragment_ecg_data.xml new file mode 100644 index 0000000..7f0a434 --- /dev/null +++ b/app/src/main/res/layout/fragment_ecg_data.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_ecg_graph.xml b/app/src/main/res/layout/fragment_ecg_graph.xml new file mode 100644 index 0000000..e721ab2 --- /dev/null +++ b/app/src/main/res/layout/fragment_ecg_graph.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_fitbit_auth.xml b/app/src/main/res/layout/fragment_fitbit_auth.xml new file mode 100644 index 0000000..5ec349f --- /dev/null +++ b/app/src/main/res/layout/fragment_fitbit_auth.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_fitbit_redirect.xml b/app/src/main/res/layout/fragment_fitbit_redirect.xml new file mode 100644 index 0000000..80f6080 --- /dev/null +++ b/app/src/main/res/layout/fragment_fitbit_redirect.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + +