Initial commit - StepMates Sync Android v1.1
This commit is contained in:
commit
750efea471
1137 files changed
+142776
No files matched your search
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.health.READ_STEPS" />
|
||||
|
||||
<queries>
|
||||
<package android:name="com.google.android.apps.healthdata" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.StepMatesSync">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name="androidx.health.connect.client.HealthConnectClient"
|
||||
android:exported="true"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.stepmates.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.health.connect.client.HealthConnectClient
|
||||
import androidx.health.connect.client.PermissionController
|
||||
import androidx.health.connect.client.permission.HealthPermission
|
||||
import androidx.health.connect.client.records.StepsRecord
|
||||
import androidx.health.connect.client.request.ReadRecordsRequest
|
||||
import androidx.health.connect.client.time.TimeRangeFilter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.stepmates.sync.databinding.ActivityMainBinding
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private lateinit var permissionLauncher: ActivityResultLauncher<Set<String>>
|
||||
private val httpClient = OkHttpClient()
|
||||
|
||||
private val PERMISSIONS = setOf(HealthPermission.getReadPermission(StepsRecord::class))
|
||||
|
||||
private val INTERVAL_LABELS = listOf("Every 5 minutes", "Every 15 minutes", "Every 30 minutes (default)", "Every hour", "Every 6 hours", "Manual only")
|
||||
private val INTERVAL_MINUTES = listOf(5L, 15L, 30L, 60L, 360L, -1L)
|
||||
private val DEFAULT_INTERVAL_INDEX = 2
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
prefs = getSharedPreferences("stepmates", Context.MODE_PRIVATE)
|
||||
|
||||
permissionLauncher = registerForActivityResult(
|
||||
PermissionController.createRequestPermissionResultContract()
|
||||
) { granted ->
|
||||
if (granted.containsAll(PERMISSIONS)) {
|
||||
binding.btnGrantPermission.visibility = android.view.View.GONE
|
||||
showSuccess("Permission granted! ✓\nTap Sync Now to sync your steps.")
|
||||
scheduleBackgroundSync()
|
||||
} else {
|
||||
binding.btnGrantPermission.visibility = android.view.View.VISIBLE
|
||||
showError("Permission denied.\nTap below to try again.")
|
||||
}
|
||||
}
|
||||
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, INTERVAL_LABELS)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binding.spinnerInterval.adapter = adapter
|
||||
|
||||
val savedIndex = prefs.getInt("interval_index", DEFAULT_INTERVAL_INDEX)
|
||||
binding.spinnerInterval.setSelection(savedIndex)
|
||||
|
||||
binding.spinnerInterval.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: android.widget.AdapterView<*>?, view: android.view.View?, position: Int, id: Long) {
|
||||
prefs.edit().putInt("interval_index", position).apply()
|
||||
scheduleBackgroundSync()
|
||||
}
|
||||
override fun onNothingSelected(parent: android.widget.AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
val savedUrl = prefs.getString("webhook_url", "") ?: ""
|
||||
if (savedUrl.isNotEmpty()) binding.etEmail.setText(savedUrl)
|
||||
|
||||
val lastSync = prefs.getLong("last_sync", 0L)
|
||||
if (lastSync > 0) {
|
||||
val fmt = SimpleDateFormat("MMM d, yyyy h:mm a", Locale.getDefault())
|
||||
binding.tvLastSync.text = "Last synced: ${fmt.format(Date(lastSync))}"
|
||||
}
|
||||
|
||||
binding.btnSync.setOnClickListener {
|
||||
val url = binding.etEmail.text.toString().trim()
|
||||
if (!isValidWebhookUrl(url)) {
|
||||
showError("Please paste your StepMates webhook URL.\nIt should start with https://webhook-")
|
||||
return@setOnClickListener
|
||||
}
|
||||
prefs.edit().putString("webhook_url", url).apply()
|
||||
hideKeyboard()
|
||||
checkHealthConnectAndSync(url)
|
||||
}
|
||||
|
||||
binding.btnGrantPermission.setOnClickListener {
|
||||
val status = HealthConnectClient.getSdkStatus(this)
|
||||
if (status == HealthConnectClient.SDK_UNAVAILABLE || status == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata")))
|
||||
} else {
|
||||
permissionLauncher.launch(PERMISSIONS)
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnDashboard.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://justinoros.github.io/step-tracker")))
|
||||
}
|
||||
|
||||
binding.etEmail.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) { binding.btnSync.performClick(); true } else false
|
||||
}
|
||||
|
||||
checkHealthConnectStatus()
|
||||
}
|
||||
|
||||
private fun checkHealthConnectStatus() {
|
||||
val status = HealthConnectClient.getSdkStatus(this)
|
||||
if (status == HealthConnectClient.SDK_UNAVAILABLE || status == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
|
||||
binding.btnGrantPermission.text = "Install Health Connect"
|
||||
binding.btnGrantPermission.visibility = android.view.View.VISIBLE
|
||||
showError("Health Connect is not installed.\nTap below to install it from the Play Store.")
|
||||
return
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
val client = HealthConnectClient.getOrCreate(this@MainActivity)
|
||||
val granted = client.permissionController.getGrantedPermissions()
|
||||
if (!granted.containsAll(PERMISSIONS)) {
|
||||
binding.btnGrantPermission.text = "Grant Health Connect Permission"
|
||||
binding.btnGrantPermission.visibility = android.view.View.VISIBLE
|
||||
showError("Steps permission not granted.\nTap below to grant access.")
|
||||
} else {
|
||||
binding.btnGrantPermission.visibility = android.view.View.GONE
|
||||
scheduleBackgroundSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleBackgroundSync() {
|
||||
val intervalIndex = prefs.getInt("interval_index", DEFAULT_INTERVAL_INDEX)
|
||||
val intervalMinutes = INTERVAL_MINUTES[intervalIndex]
|
||||
val workManager = WorkManager.getInstance(this)
|
||||
|
||||
if (intervalMinutes == -1L) {
|
||||
workManager.cancelUniqueWork("stepmates_sync")
|
||||
return
|
||||
}
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<StepSyncWorker>(intervalMinutes, TimeUnit.MINUTES)
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork("stepmates_sync", ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
}
|
||||
|
||||
private fun isValidWebhookUrl(url: String) = url.startsWith("https://") && url.contains("token=")
|
||||
|
||||
private fun checkHealthConnectAndSync(webhookUrl: String) {
|
||||
val status = HealthConnectClient.getSdkStatus(this)
|
||||
if (status == HealthConnectClient.SDK_UNAVAILABLE || status == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
|
||||
binding.btnGrantPermission.text = "Install Health Connect"
|
||||
binding.btnGrantPermission.visibility = android.view.View.VISIBLE
|
||||
showError("Health Connect is not installed.")
|
||||
return
|
||||
}
|
||||
val client = HealthConnectClient.getOrCreate(this)
|
||||
setLoading(true)
|
||||
showStatus("Checking Health Connect permissions…")
|
||||
lifecycleScope.launch {
|
||||
val granted = client.permissionController.getGrantedPermissions()
|
||||
if (!granted.containsAll(PERMISSIONS)) {
|
||||
setLoading(false)
|
||||
prefs.edit().putString("pending_url", webhookUrl).apply()
|
||||
binding.btnGrantPermission.text = "Grant Health Connect Permission"
|
||||
binding.btnGrantPermission.visibility = android.view.View.VISIBLE
|
||||
showError("Steps permission not granted.\nTap below to grant access.")
|
||||
permissionLauncher.launch(PERMISSIONS)
|
||||
} else {
|
||||
binding.btnGrantPermission.visibility = android.view.View.GONE
|
||||
syncSteps(client, webhookUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncSteps(client: HealthConnectClient, webhookUrl: String) {
|
||||
showStatus("Reading your steps…")
|
||||
try {
|
||||
val today = LocalDate.now(ZoneId.systemDefault())
|
||||
val stepsByDate = mutableMapOf<String, Long>()
|
||||
for (i in 0..6) {
|
||||
val date = today.minusDays(i.toLong())
|
||||
val startTime = date.atStartOfDay(ZoneId.systemDefault()).toInstant()
|
||||
val endTime = if (i == 0) Instant.now() else date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()
|
||||
val response = client.readRecords(ReadRecordsRequest(StepsRecord::class, timeRangeFilter = TimeRangeFilter.between(startTime, endTime)))
|
||||
val total = response.records.sumOf { it.count }
|
||||
if (total > 0) {
|
||||
val dateKey = "${date.year}-${date.monthValue.toString().padStart(2,'0')}-${date.dayOfMonth.toString().padStart(2,'0')}"
|
||||
stepsByDate[dateKey] = total
|
||||
}
|
||||
}
|
||||
if (stepsByDate.isEmpty()) { setLoading(false); showStatus("No step data found in Health Connect.\nMake sure your fitness app syncs to Health Connect."); return }
|
||||
showStatus("Syncing ${stepsByDate.values.sum().toLocaleString()} steps…")
|
||||
val metricsArray = JSONArray()
|
||||
val stepsMetric = JSONObject()
|
||||
stepsMetric.put("name", "steps")
|
||||
val dataArray = JSONArray()
|
||||
for ((dateStr, count) in stepsByDate) { val entry = JSONObject(); entry.put("date", dateStr); entry.put("qty", count); dataArray.put(entry) }
|
||||
stepsMetric.put("data", dataArray)
|
||||
metricsArray.put(stepsMetric)
|
||||
val payload = JSONObject()
|
||||
val dataObj = JSONObject()
|
||||
dataObj.put("metrics", metricsArray)
|
||||
payload.put("data", dataObj)
|
||||
val body = payload.toString().toRequestBody("application/json".toMediaType())
|
||||
val request = Request.Builder().url(webhookUrl).post(body).build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
setLoading(false)
|
||||
if (response.isSuccessful) {
|
||||
val todayKey = "${today.year}-${today.monthValue.toString().padStart(2,'0')}-${today.dayOfMonth.toString().padStart(2,'0')}"
|
||||
val todaySteps = stepsByDate[todayKey] ?: 0L
|
||||
val now = System.currentTimeMillis()
|
||||
prefs.edit().putLong("last_sync", now).apply()
|
||||
val fmt = SimpleDateFormat("MMM d, yyyy h:mm a", Locale.getDefault())
|
||||
binding.tvLastSync.text = "Last synced: ${fmt.format(Date(now))}"
|
||||
showSuccess("✓ Synced!\n\nToday: ${todaySteps.toLocaleString()} steps")
|
||||
scheduleBackgroundSync()
|
||||
} else { showError("Sync failed (${response.code}).\nCheck your webhook URL and try again.") }
|
||||
} catch (e: Exception) { setLoading(false); showError("Failed to read steps.\n${e.message}") }
|
||||
}
|
||||
|
||||
private fun setLoading(loading: Boolean) {
|
||||
runOnUiThread {
|
||||
binding.progressBar.visibility = if (loading) android.view.View.VISIBLE else android.view.View.GONE
|
||||
binding.btnSync.isEnabled = !loading
|
||||
binding.etEmail.isEnabled = !loading
|
||||
}
|
||||
}
|
||||
|
||||
private fun showStatus(msg: String) { runOnUiThread { binding.tvStatus.text = msg; binding.tvStatus.setTextColor(getColor(android.R.color.white)) } }
|
||||
private fun showError(msg: String) { runOnUiThread { binding.tvStatus.text = msg; binding.tvStatus.setTextColor(0xFFFF6B6B.toInt()) } }
|
||||
private fun showSuccess(msg: String) { runOnUiThread { binding.tvStatus.text = msg; binding.tvStatus.setTextColor(0xFF4ECA8B.toInt()) } }
|
||||
private fun hideKeyboard() { val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; imm.hideSoftInputFromWindow(binding.root.windowToken, 0) }
|
||||
private fun Long.toLocaleString() = String.format(Locale.getDefault(), "%,d", this)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.stepmates.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.health.connect.client.HealthConnectClient
|
||||
import androidx.health.connect.client.permission.HealthPermission
|
||||
import androidx.health.connect.client.records.StepsRecord
|
||||
import androidx.health.connect.client.request.ReadRecordsRequest
|
||||
import androidx.health.connect.client.time.TimeRangeFilter
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class StepSyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
|
||||
private val httpClient = OkHttpClient()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val prefs = applicationContext.getSharedPreferences("stepmates", Context.MODE_PRIVATE)
|
||||
val webhookUrl = prefs.getString("webhook_url", null) ?: return Result.failure()
|
||||
|
||||
val availabilityStatus = HealthConnectClient.getSdkStatus(applicationContext)
|
||||
if (availabilityStatus != HealthConnectClient.SDK_AVAILABLE) return Result.failure()
|
||||
|
||||
val client = HealthConnectClient.getOrCreate(applicationContext)
|
||||
val granted = client.permissionController.getGrantedPermissions()
|
||||
if (!granted.contains(HealthPermission.getReadPermission(StepsRecord::class))) return Result.failure()
|
||||
|
||||
return try {
|
||||
val today = LocalDate.now(ZoneId.systemDefault())
|
||||
val stepsByDate = mutableMapOf<String, Long>()
|
||||
|
||||
for (i in 0..6) {
|
||||
val date = today.minusDays(i.toLong())
|
||||
val startTime = date.atStartOfDay(ZoneId.systemDefault()).toInstant()
|
||||
val endTime = if (i == 0) Instant.now() else date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()
|
||||
val response = client.readRecords(ReadRecordsRequest(StepsRecord::class, timeRangeFilter = TimeRangeFilter.between(startTime, endTime)))
|
||||
val total = response.records.sumOf { it.count }
|
||||
if (total > 0) {
|
||||
val dateKey = "${date.year}-${date.monthValue.toString().padStart(2,'0')}-${date.dayOfMonth.toString().padStart(2,'0')}"
|
||||
stepsByDate[dateKey] = total
|
||||
}
|
||||
}
|
||||
|
||||
if (stepsByDate.isEmpty()) return Result.success()
|
||||
|
||||
val metricsArray = JSONArray()
|
||||
val stepsMetric = JSONObject()
|
||||
stepsMetric.put("name", "steps")
|
||||
val dataArray = JSONArray()
|
||||
for ((dateStr, count) in stepsByDate) {
|
||||
val entry = JSONObject()
|
||||
entry.put("date", dateStr)
|
||||
entry.put("qty", count)
|
||||
dataArray.put(entry)
|
||||
}
|
||||
stepsMetric.put("data", dataArray)
|
||||
metricsArray.put(stepsMetric)
|
||||
|
||||
val payload = JSONObject()
|
||||
val dataObj = JSONObject()
|
||||
dataObj.put("metrics", metricsArray)
|
||||
payload.put("data", dataObj)
|
||||
|
||||
val body = payload.toString().toRequestBody("application/json".toMediaType())
|
||||
val request = Request.Builder().url(webhookUrl).post(body).build()
|
||||
val httpResponse = httpClient.newCall(request).execute()
|
||||
|
||||
if (httpResponse.isSuccessful) {
|
||||
val now = System.currentTimeMillis()
|
||||
prefs.edit().putLong("last_sync", now).apply()
|
||||
Result.success()
|
||||
} else {
|
||||
Result.retry()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#182433" />
|
||||
<stroke android:width="1.5dp" android:color="#2A3D52" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,195 @@
|
||||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#0F1923"
|
||||
android:padding="28dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="StepMates"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="#00C9B1"
|
||||
android:letterSpacing="-0.03"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginTop="48dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTagline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Step sync for Android"
|
||||
android:textSize="15sp"
|
||||
android:textColor="#8899A6"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvLogo"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="YOUR STEPMATES WEBHOOK URL"
|
||||
android:textSize="11sp"
|
||||
android:textColor="#8899A6"
|
||||
android:letterSpacing="0.08"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvTagline"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginTop="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etEmail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:inputType="textUri"
|
||||
android:hint="https://webhook-…?token=…"
|
||||
android:textColorHint="#8899A6"
|
||||
android:textColor="#F8F9FA"
|
||||
android:textSize="13sp"
|
||||
android:background="@drawable/input_background"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:imeOptions="actionDone"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvEmailLabel"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvIntervalLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="AUTO-SYNC INTERVAL"
|
||||
android:textSize="11sp"
|
||||
android:textColor="#8899A6"
|
||||
android:letterSpacing="0.08"
|
||||
app:layout_constraintTop_toBottomOf="@id/etEmail"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginTop="20dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerInterval"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:background="@drawable/input_background"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:popupBackground="#182433"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvIntervalLabel"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llButtons"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/spinnerInterval"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSync"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Sync Now"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#0F1923"
|
||||
android:backgroundTint="#00C9B1"
|
||||
android:stateListAnimator="@null"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDashboard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Dashboard"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#00C9B1"
|
||||
android:backgroundTint="#182433"
|
||||
android:stateListAnimator="@null" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGrantPermission"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:text="Grant Health Connect Permission"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#0F1923"
|
||||
android:backgroundTint="#FFB347"
|
||||
android:stateListAnimator="@null"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/llButtons"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="10dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:indeterminateTint="#00C9B1"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnGrantPermission"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStatus"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="15sp"
|
||||
android:textColor="#F8F9FA"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLastSync"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="13sp"
|
||||
android:textColor="#8899A6"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvStatus"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFooter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_url"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#2A3D52"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">StepMates Sync</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.StepMatesSync" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">#00C9B1</item>
|
||||
<item name="colorPrimaryVariant">#0F1923</item>
|
||||
<item name="colorOnPrimary">#0F1923</item>
|
||||
<item name="android:windowBackground">#0F1923</item>
|
||||
<item name="android:statusBarColor">#0F1923</item>
|
||||
<item name="android:navigationBarColor">#0F1923</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in new issue
Block a user