Merge pull request #641 from nightscout/openhumans_v2
Update Open Humans uploader for new database
This commit is contained in:
commit
c9897da78d
|
@ -191,6 +191,7 @@ dependencies {
|
|||
implementation project(':omnipod-eros')
|
||||
implementation project(':omnipod-dash')
|
||||
implementation project(':diaconn')
|
||||
implementation project(':openhumans')
|
||||
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
|
||||
|
|
|
@ -243,20 +243,6 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".plugins.general.openhumans.OpenHumansLoginActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="setup-openhumans"
|
||||
android:scheme="androidaps" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
|
||||
|
||||
</application>
|
||||
|
|
|
@ -20,6 +20,7 @@ import info.nightscout.androidaps.events.EventPreferenceChange
|
|||
import info.nightscout.androidaps.events.EventRebuildTabs
|
||||
import info.nightscout.androidaps.interfaces.PluginBase
|
||||
import info.nightscout.androidaps.interfaces.ProfileFunction
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansUploader
|
||||
import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin
|
||||
import info.nightscout.androidaps.plugins.aps.openAPSAMA.OpenAPSAMAPlugin
|
||||
import info.nightscout.androidaps.plugins.aps.openAPSSMB.OpenAPSSMBPlugin
|
||||
|
@ -30,7 +31,6 @@ import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin
|
|||
import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin
|
||||
import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin
|
||||
import info.nightscout.androidaps.plugins.general.nsclient.data.NSSettingsStatus
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin
|
||||
import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin
|
||||
import info.nightscout.androidaps.plugins.general.wear.WearPlugin
|
||||
|
|
|
@ -6,7 +6,6 @@ import info.nightscout.androidaps.MainActivity
|
|||
import info.nightscout.androidaps.activities.*
|
||||
import info.nightscout.androidaps.activities.HistoryBrowseActivity
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.activities.LogSettingActivity
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansLoginActivity
|
||||
import info.nightscout.androidaps.plugins.general.overview.activities.QuickWizardListActivity
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity
|
||||
import info.nightscout.androidaps.setupwizard.SetupWizardActivity
|
||||
|
@ -28,6 +27,5 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector abstract fun contributesStatsActivity(): StatsActivity
|
||||
@ContributesAndroidInjector abstract fun contributesSurveyActivity(): SurveyActivity
|
||||
@ContributesAndroidInjector abstract fun contributesDefaultProfileActivity(): ProfileHelperActivity
|
||||
@ContributesAndroidInjector abstract fun contributesOpenHumansLoginActivity(): OpenHumansLoginActivity
|
||||
|
||||
}
|
|
@ -16,6 +16,7 @@ import info.nightscout.androidaps.di.CoreModule
|
|||
import info.nightscout.androidaps.diaconn.di.DiaconnG8Module
|
||||
import info.nightscout.androidaps.insight.di.InsightDatabaseModule
|
||||
import info.nightscout.androidaps.insight.di.InsightModule
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.dagger.OpenHumansModule
|
||||
import info.nightscout.androidaps.plugins.pump.common.di.PumpCommonModule
|
||||
import info.nightscout.androidaps.plugins.pump.common.di.RileyLinkModule
|
||||
import info.nightscout.androidaps.plugins.pump.medtronic.di.MedtronicModule
|
||||
|
@ -57,8 +58,8 @@ import javax.inject.Singleton
|
|||
InsightModule::class,
|
||||
InsightDatabaseModule::class,
|
||||
WorkersModule::class,
|
||||
OHUploaderModule::class,
|
||||
DiaconnG8Module::class
|
||||
DiaconnG8Module::class,
|
||||
OpenHumansModule::class
|
||||
]
|
||||
)
|
||||
interface AppComponent : AndroidInjector<MainApp> {
|
||||
|
|
|
@ -20,8 +20,6 @@ import info.nightscout.androidaps.plugins.general.automation.dialogs.EditTrigger
|
|||
import info.nightscout.androidaps.plugins.general.food.FoodFragment
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.MaintenanceFragment
|
||||
import info.nightscout.androidaps.plugins.general.nsclient.NSClientFragment
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansFragment
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansLoginActivity
|
||||
import info.nightscout.androidaps.plugins.general.overview.OverviewFragment
|
||||
import info.nightscout.androidaps.plugins.general.overview.dialogs.EditQuickWizardDialog
|
||||
import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorFragment
|
||||
|
@ -70,8 +68,6 @@ abstract class FragmentsModule {
|
|||
|
||||
@ContributesAndroidInjector abstract fun contributesVirtualPumpFragment(): VirtualPumpFragment
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesOpenHumansFragment(): OpenHumansFragment
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesCalibrationDialog(): CalibrationDialog
|
||||
@ContributesAndroidInjector abstract fun contributesCarbsDialog(): CarbsDialog
|
||||
@ContributesAndroidInjector abstract fun contributesCareDialog(): CareDialog
|
||||
|
@ -95,7 +91,5 @@ abstract class FragmentsModule {
|
|||
@ContributesAndroidInjector abstract fun contributesWizardDialog(): WizardDialog
|
||||
@ContributesAndroidInjector abstract fun contributesWizardInfoDialog(): WizardInfoDialog
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesExchangeAuthTokenDialog(): OpenHumansLoginActivity.ExchangeAuthTokenDialog
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesPasswordCheck(): PasswordCheck
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package info.nightscout.androidaps.dependencyInjection
|
||||
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OHUploadWorker
|
||||
|
||||
@Module
|
||||
@Suppress("unused")
|
||||
abstract class OHUploaderModule {
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesOHUploadWorkerInjector(): OHUploadWorker
|
||||
}
|
|
@ -10,6 +10,7 @@ import info.nightscout.androidaps.danar.DanaRPlugin
|
|||
import info.nightscout.androidaps.danars.DanaRSPlugin
|
||||
import info.nightscout.androidaps.diaconn.DiaconnG8Plugin
|
||||
import info.nightscout.androidaps.interfaces.PluginBase
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansUploader
|
||||
import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin
|
||||
import info.nightscout.androidaps.plugins.aps.openAPSAMA.OpenAPSAMAPlugin
|
||||
import info.nightscout.androidaps.plugins.aps.openAPSSMB.OpenAPSSMBPlugin
|
||||
|
@ -345,6 +346,12 @@ abstract class PluginsModule {
|
|||
// @IntKey(480)
|
||||
// abstract fun bindOpenHumansPlugin(plugin: OpenHumansUploader): PluginBase
|
||||
|
||||
@Binds
|
||||
@NotNSClient
|
||||
@IntoMap
|
||||
@IntKey(480)
|
||||
abstract fun bindsOpenHumansPlugin(plugin: OpenHumansUploader): PluginBase
|
||||
|
||||
@Binds
|
||||
@AllConfigs
|
||||
@IntoMap
|
||||
|
|
|
@ -17,7 +17,6 @@ import info.nightscout.androidaps.plugins.aps.logger.LoggerCallback
|
|||
import info.nightscout.androidaps.plugins.aps.loop.ScriptReader
|
||||
import info.nightscout.androidaps.plugins.aps.openAPSSMB.SMBDefaults
|
||||
import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import org.json.JSONArray
|
||||
|
@ -40,7 +39,6 @@ class DetermineBasalAdapterAMAJS internal constructor(scriptReader: ScriptReader
|
|||
@Inject lateinit var sp: SP
|
||||
@Inject lateinit var profileFunction: ProfileFunction
|
||||
@Inject lateinit var iobCobCalculator: IobCobCalculator
|
||||
@Inject lateinit var openHumansUploader: OpenHumansUploader
|
||||
|
||||
private val mScriptReader: ScriptReader
|
||||
private var profile = JSONObject()
|
||||
|
@ -116,7 +114,6 @@ class DetermineBasalAdapterAMAJS internal constructor(scriptReader: ScriptReader
|
|||
aapsLogger.debug(LTag.APS, "Result: $result")
|
||||
try {
|
||||
val resultJson = JSONObject(result)
|
||||
openHumansUploader.enqueueAMAData(profile, glucoseStatus, iobData, mealData, currentTemp, autosensData, resultJson)
|
||||
determineBasalResultAMA = DetermineBasalResultAMA(injector, jsResult, resultJson)
|
||||
} catch (e: JSONException) {
|
||||
aapsLogger.error(LTag.APS, "Unhandled exception", e)
|
||||
|
|
|
@ -17,7 +17,6 @@ import info.nightscout.androidaps.logging.LTag
|
|||
import info.nightscout.androidaps.plugins.aps.logger.LoggerCallback
|
||||
import info.nightscout.androidaps.plugins.aps.loop.ScriptReader
|
||||
import info.nightscout.androidaps.plugins.configBuilder.ConstraintChecker
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader
|
||||
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus
|
||||
import info.nightscout.androidaps.utils.SafeParse
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
|
@ -41,7 +40,6 @@ class DetermineBasalAdapterSMBJS internal constructor(private val scriptReader:
|
|||
@Inject lateinit var profileFunction: ProfileFunction
|
||||
@Inject lateinit var iobCobCalculator: IobCobCalculator
|
||||
@Inject lateinit var activePlugin: ActivePlugin
|
||||
@Inject lateinit var openHumansUploader: OpenHumansUploader
|
||||
|
||||
private var profile = JSONObject()
|
||||
private var mGlucoseStatus = JSONObject()
|
||||
|
@ -129,7 +127,6 @@ class DetermineBasalAdapterSMBJS internal constructor(private val scriptReader:
|
|||
aapsLogger.debug(LTag.APS, "Result: $result")
|
||||
try {
|
||||
val resultJson = JSONObject(result)
|
||||
openHumansUploader.enqueueSMBData(profile, mGlucoseStatus, iobData, mealData, currentTemp, autosensData, microBolusAllowed, smbAlwaysAllowed, resultJson)
|
||||
determineBasalResultSMB = DetermineBasalResultSMB(injector, resultJson)
|
||||
} catch (e: JSONException) {
|
||||
aapsLogger.error(LTag.APS, "Unhandled exception", e)
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.RxWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import info.nightscout.androidaps.MainApp
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader.Companion.NOTIFICATION_CHANNEL
|
||||
import info.nightscout.androidaps.plugins.general.openhumans.OpenHumansUploader.Companion.UPLOAD_NOTIFICATION_ID
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import io.reactivex.Single
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class OHUploadWorker(context: Context, workerParameters: WorkerParameters)
|
||||
: RxWorker(context, workerParameters) {
|
||||
|
||||
@Inject
|
||||
lateinit var sp: SP
|
||||
|
||||
@Inject
|
||||
lateinit var openHumansUploader: OpenHumansUploader
|
||||
|
||||
@Inject
|
||||
lateinit var resourceHelper: ResourceHelper
|
||||
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
override fun createWork(): Single<Result> =Single.just(Result.success())
|
||||
/*
|
||||
= Single.defer {
|
||||
|
||||
// Here we inject every time we create work
|
||||
// We could build our own WorkerFactory with dagger but this will create conflicts with other Workers
|
||||
// (see https://medium.com/wonderquill/how-to-pass-custom-parameters-to-rxworker-worker-using-dagger-2-f4cfbc9892ba)
|
||||
// This class will be replaced with new DB
|
||||
|
||||
(applicationContext as MainApp).androidInjector().inject(this)
|
||||
|
||||
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
||||
val wifiOnly = sp.getBoolean("key_oh_wifi_only", true)
|
||||
val isConnectedToWifi = wifiManager?.isWifiEnabled ?: false && wifiManager?.connectionInfo?.networkId != -1
|
||||
if (!wifiOnly || (wifiOnly && isConnectedToWifi)) {
|
||||
setForegroundAsync(createForegroundInfo())
|
||||
openHumansUploader.uploadDataSegmentally()
|
||||
.andThen(Single.just(Result.success()))
|
||||
.onErrorResumeNext { Single.just(Result.retry()) }
|
||||
} else {
|
||||
Single.just(Result.retry())
|
||||
}
|
||||
}
|
||||
*/
|
||||
private fun createForegroundInfo(): ForegroundInfo {
|
||||
val title = resourceHelper.gs(info.nightscout.androidaps.R.string.open_humans)
|
||||
|
||||
val notification: Notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
|
||||
.setContentTitle(title)
|
||||
.setTicker(title)
|
||||
.setContentText(resourceHelper.gs(info.nightscout.androidaps.R.string.your_phone_is_upload_data))
|
||||
.setSmallIcon(info.nightscout.androidaps.R.drawable.notif_icon)
|
||||
.setOngoing(true)
|
||||
.setProgress(0, 0 , true)
|
||||
.build()
|
||||
return ForegroundInfo(UPLOAD_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Base64
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposables
|
||||
import okhttp3.*
|
||||
import okio.BufferedSink
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class OpenHumansAPI(
|
||||
private val baseUrl: String,
|
||||
clientId: String,
|
||||
clientSecret: String,
|
||||
private val redirectUri: String
|
||||
) {
|
||||
|
||||
private val authHeader = "Basic " + Base64.encodeToString("$clientId:$clientSecret".toByteArray(), Base64.NO_WRAP)
|
||||
private val client = OkHttpClient()
|
||||
|
||||
fun exchangeAuthToken(code: String): Single<OAuthTokens> = sendTokenRequest(FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("redirect_uri", redirectUri)
|
||||
.add("code", code)
|
||||
.build())
|
||||
|
||||
fun refreshAccessToken(refreshToken: String): Single<OAuthTokens> = sendTokenRequest(FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("redirect_uri", redirectUri)
|
||||
.add("refresh_token", refreshToken)
|
||||
.build())
|
||||
|
||||
private fun sendTokenRequest(body: FormBody) = Request.Builder()
|
||||
.url("$baseUrl/oauth2/token/")
|
||||
.addHeader("Authorization", authHeader)
|
||||
.post(body)
|
||||
.build()
|
||||
.toSingle()
|
||||
.map { response ->
|
||||
response.use { _ ->
|
||||
val responseBody = response.body
|
||||
val jsonObject = responseBody?.let { JSONObject(it.string()) }
|
||||
if (!response.isSuccessful) throw OHHttpException(response.code, response.message, jsonObject?.getString("error"))
|
||||
if (jsonObject == null) throw OHHttpException(response.code, response.message, "No body")
|
||||
if (!jsonObject.has("expires_in")) throw OHMissingFieldException("expires_in")
|
||||
OAuthTokens(
|
||||
accessToken = jsonObject.getString("access_token")
|
||||
?: throw OHMissingFieldException("access_token"),
|
||||
refreshToken = jsonObject.getString("refresh_token")
|
||||
?: throw OHMissingFieldException("refresh_token"),
|
||||
expiresAt = response.sentRequestAtMillis + jsonObject.getInt("expires_in") * 1000L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getProjectMemberId(accessToken: String): Single<String> = Request.Builder()
|
||||
.url("$baseUrl/api/direct-sharing/project/exchange-member/?access_token=$accessToken")
|
||||
.get()
|
||||
.build()
|
||||
.toSingle()
|
||||
.map {
|
||||
it.jsonBody.getString("project_member_id")
|
||||
?: throw OHMissingFieldException("project_member_id")
|
||||
}
|
||||
|
||||
fun prepareFileUpload(accessToken: String, fileName: String, metadata: FileMetadata): Single<PreparedUpload> = Request.Builder()
|
||||
.url("$baseUrl/api/direct-sharing/project/files/upload/direct/?access_token=$accessToken")
|
||||
.post(FormBody.Builder()
|
||||
.add("filename", fileName)
|
||||
.add("metadata", metadata.toJSON().toString())
|
||||
.build())
|
||||
.build()
|
||||
.toSingle()
|
||||
.map {
|
||||
val json = it.jsonBody
|
||||
PreparedUpload(
|
||||
fileId = json.getString("id") ?: throw OHMissingFieldException("id"),
|
||||
uploadURL = json.getString("url") ?: throw OHMissingFieldException("url")
|
||||
)
|
||||
}
|
||||
|
||||
fun uploadFile(url: String, content: ByteArray): Completable = Request.Builder()
|
||||
.url(url)
|
||||
.put(object : RequestBody() {
|
||||
override fun contentType(): MediaType? = null
|
||||
|
||||
override fun contentLength() = content.size.toLong()
|
||||
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
sink.write(content)
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.toSingle()
|
||||
.doOnSuccess { response ->
|
||||
response.use { _ ->
|
||||
if (!response.isSuccessful) throw OHHttpException(response.code, response.message, null)
|
||||
}
|
||||
}
|
||||
.ignoreElement()
|
||||
|
||||
fun completeFileUpload(accessToken: String, fileId: String): Completable = Request.Builder()
|
||||
.url("$baseUrl/api/direct-sharing/project/files/upload/complete/?access_token=$accessToken")
|
||||
.post(FormBody.Builder()
|
||||
.add("file_id", fileId)
|
||||
.build())
|
||||
.build()
|
||||
.toSingle()
|
||||
.doOnSuccess { it.jsonBody }
|
||||
.ignoreElement()
|
||||
|
||||
private fun Request.toSingle() = Single.create<Response> {
|
||||
val call = client.newCall(this)
|
||||
call.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
it.tryOnError(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
it.onSuccess(response)
|
||||
}
|
||||
})
|
||||
it.setDisposable(Disposables.fromRunnable { call.cancel() })
|
||||
}
|
||||
|
||||
private val Response.jsonBody
|
||||
get() = use { _ ->
|
||||
val jsonObject = body?.let { JSONObject(it.string()) }
|
||||
?: throw OHHttpException(code, message, null)
|
||||
if (!isSuccessful) throw OHHttpException(code, message, jsonObject.getString("detail"))
|
||||
jsonObject
|
||||
}
|
||||
|
||||
data class OAuthTokens(
|
||||
val accessToken: String,
|
||||
val refreshToken: String,
|
||||
val expiresAt: Long
|
||||
)
|
||||
|
||||
data class FileMetadata(
|
||||
val tags: List<String>,
|
||||
val description: String,
|
||||
val md5: String? = null,
|
||||
val creationDate: Long? = null,
|
||||
val startDate: Long? = null,
|
||||
val endDate: Long? = null
|
||||
) {
|
||||
|
||||
fun toJSON(): JSONObject {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("tags", JSONArray().apply { tags.forEach { put(it) } })
|
||||
jsonObject.put("description", description)
|
||||
jsonObject.put("md5", md5)
|
||||
creationDate?.let { jsonObject.put("creation_date", iso8601DateFormatter.format(Date(it))) }
|
||||
startDate?.let { jsonObject.put("start_date", iso8601DateFormatter.format(Date(it))) }
|
||||
endDate?.let { jsonObject.put("end_date", iso8601DateFormatter.format(Date(it))) }
|
||||
return jsonObject
|
||||
}
|
||||
}
|
||||
|
||||
data class PreparedUpload(
|
||||
val fileId: String,
|
||||
val uploadURL: String
|
||||
)
|
||||
|
||||
data class OHHttpException(
|
||||
val code: Int,
|
||||
val meaning: String,
|
||||
val detail: String?
|
||||
) : RuntimeException() {
|
||||
|
||||
override val message: String get() = toString()
|
||||
}
|
||||
|
||||
data class OHMissingFieldException(
|
||||
val name: String
|
||||
) : RuntimeException() {
|
||||
|
||||
override val message: String get() = toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private val iso8601DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.work.WorkManager
|
||||
import dagger.android.support.DaggerFragment
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.events.Event
|
||||
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
||||
import info.nightscout.androidaps.utils.alertDialogs.OKDialog
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import info.nightscout.androidaps.utils.rx.AapsSchedulers
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.rxkotlin.plusAssign
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class OpenHumansFragment : DaggerFragment() {
|
||||
|
||||
private var viewsCreated = false
|
||||
private var login: Button? = null
|
||||
private var logout: Button? = null
|
||||
private var memberId: TextView? = null
|
||||
private var queueSize: TextView? = null
|
||||
private var workerState: TextView? = null
|
||||
private var queueSizeValue = 0L
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
@Inject lateinit var rxBus: RxBusWrapper
|
||||
@Inject lateinit var openHumansUploader: OpenHumansUploader
|
||||
@Inject lateinit var resourceHelper: ResourceHelper
|
||||
@Inject lateinit var aapsSchedulers: AapsSchedulers
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// compositeDisposable += Single.fromCallable { databaseHelper.getOHQueueSize() }
|
||||
// .subscribeOn(aapsSchedulers.io)
|
||||
// .repeatWhen {
|
||||
// rxBus.toObservable(UpdateViewEvent::class.java)
|
||||
// .cast(Any::class.java)
|
||||
// .mergeWith(rxBus.toObservable(UpdateQueueEvent::class.java)
|
||||
// .throttleLatest(5, TimeUnit.SECONDS))
|
||||
// .toFlowable(BackpressureStrategy.LATEST)
|
||||
// }
|
||||
// .observeOn(aapsSchedulers.main)
|
||||
// .subscribe({
|
||||
// queueSizeValue = it
|
||||
// updateGUI()
|
||||
// }, {})
|
||||
context?.applicationContext?.let { appContext ->
|
||||
WorkManager.getInstance(appContext).getWorkInfosForUniqueWorkLiveData(OpenHumansUploader.WORK_NAME).observe(this, {
|
||||
val workInfo = it.lastOrNull()
|
||||
if (workInfo == null) {
|
||||
workerState?.visibility = View.GONE
|
||||
} else {
|
||||
workerState?.visibility = View.VISIBLE
|
||||
workerState?.text = getString(R.string.worker_state, workInfo.state.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
compositeDisposable.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = inflater.inflate(R.layout.fragment_open_humans, container, false)
|
||||
login = view.findViewById(R.id.login)
|
||||
logout = view.findViewById(R.id.logout)
|
||||
memberId = view.findViewById(R.id.member_id)
|
||||
queueSize = view.findViewById(R.id.queue_size)
|
||||
workerState = view.findViewById(R.id.worker_state)
|
||||
login!!.setOnClickListener { startActivity(Intent(context, OpenHumansLoginActivity::class.java)) }
|
||||
logout!!.setOnClickListener {
|
||||
activity?.let { activity -> OKDialog.showConfirmation(activity, resourceHelper.gs(R.string.oh_logout_confirmation)) { openHumansUploader.logout() } }
|
||||
}
|
||||
viewsCreated = true
|
||||
updateGUI()
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewsCreated = false
|
||||
login = null
|
||||
logout = null
|
||||
memberId = null
|
||||
queueSize = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
fun updateGUI() {
|
||||
if (viewsCreated) {
|
||||
queueSize!!.text = getString(R.string.queue_size, queueSizeValue)
|
||||
val projectMemberId = openHumansUploader.projectMemberId
|
||||
memberId!!.text = getString(R.string.project_member_id, projectMemberId
|
||||
?: getString(R.string.not_logged_in))
|
||||
login!!.visibility = if (projectMemberId == null) View.VISIBLE else View.GONE
|
||||
logout!!.visibility = if (projectMemberId != null) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
object UpdateViewEvent : Event()
|
||||
|
||||
object UpdateQueueEvent : Event()
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import dagger.android.support.DaggerDialogFragment
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.activities.NoSplashAppCompatActivity
|
||||
import info.nightscout.androidaps.utils.rx.AapsSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import javax.inject.Inject
|
||||
|
||||
class OpenHumansLoginActivity : NoSplashAppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_open_humans_login)
|
||||
val button = findViewById<Button>(R.id.button)
|
||||
val checkbox = findViewById<CheckBox>(R.id.checkbox)
|
||||
|
||||
button.setOnClickListener {
|
||||
if (checkbox.isChecked) {
|
||||
CustomTabsIntent.Builder().build().launchUrl(this, Uri.parse(OpenHumansUploader.AUTH_URL))
|
||||
} else {
|
||||
Toast.makeText(this, R.string.you_need_to_accept_the_of_use_first, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (supportFragmentManager.fragments.size == 0 && code != null) {
|
||||
ExchangeAuthTokenDialog(code).show(supportFragmentManager, "ExchangeAuthTokenDialog")
|
||||
}
|
||||
}
|
||||
|
||||
class ExchangeAuthTokenDialog : DaggerDialogFragment() {
|
||||
|
||||
@Inject lateinit var openHumansUploader: OpenHumansUploader
|
||||
@Inject lateinit var aapsSchedulers: AapsSchedulers
|
||||
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
init {
|
||||
isCancelable = false
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.completing_login)
|
||||
.setMessage(R.string.please_wait)
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
disposable = openHumansUploader.login(arguments?.getString("authToken")!!).subscribeOn(aapsSchedulers.io).subscribe({
|
||||
dismiss()
|
||||
SetupDoneDialog().show(parentFragmentManager, "SetupDoneDialog")
|
||||
}, {
|
||||
dismiss()
|
||||
ErrorDialog(it.message).show(parentFragmentManager, "ErrorDialog")
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
disposable?.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
operator fun invoke(authToken: String): ExchangeAuthTokenDialog {
|
||||
val dialog = ExchangeAuthTokenDialog()
|
||||
val args = Bundle()
|
||||
args.putString("authToken", authToken)
|
||||
dialog.arguments = args
|
||||
return dialog
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val message = arguments?.getString("message")
|
||||
val shownMessage = if (message == null) getString(R.string.there_was_an_error)
|
||||
else "${getString(R.string.there_was_an_error)}\n\n$message"
|
||||
return AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(shownMessage)
|
||||
.setPositiveButton(R.string.close, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
operator fun invoke(message: String?): ErrorDialog {
|
||||
val dialog = ErrorDialog()
|
||||
val args = Bundle()
|
||||
args.putString("message", message)
|
||||
dialog.arguments = args
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SetupDoneDialog : DialogFragment() {
|
||||
|
||||
init {
|
||||
isCancelable = false
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.successfully_logged_in)
|
||||
.setMessage(R.string.setup_will_continue_in_background)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.close) { _, _ ->
|
||||
requireActivity().run {
|
||||
setResult(FragmentActivity.RESULT_OK)
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
.create()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,651 +0,0 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.*
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.BuildConfig
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.database.AppRepository
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.database.entities.TemporaryTarget
|
||||
import info.nightscout.androidaps.database.entities.TherapyEvent
|
||||
import info.nightscout.androidaps.db.*
|
||||
import info.nightscout.androidaps.events.EventPreferenceChange
|
||||
import info.nightscout.androidaps.extensions.toConstant
|
||||
import info.nightscout.androidaps.interfaces.PluginBase
|
||||
import info.nightscout.androidaps.interfaces.PluginDescription
|
||||
import info.nightscout.androidaps.interfaces.PluginType
|
||||
import info.nightscout.androidaps.logging.AAPSLogger
|
||||
import info.nightscout.androidaps.logging.LTag
|
||||
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import info.nightscout.androidaps.utils.rx.AapsSchedulers
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxkotlin.plusAssign
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class OpenHumansUploader @Inject constructor(
|
||||
injector: HasAndroidInjector,
|
||||
resourceHelper: ResourceHelper,
|
||||
aapsLogger: AAPSLogger,
|
||||
private val aapsSchedulers: AapsSchedulers,
|
||||
private val sp: SP,
|
||||
private val rxBus: RxBusWrapper,
|
||||
private val context: Context,
|
||||
val repository: AppRepository
|
||||
) : PluginBase(
|
||||
PluginDescription()
|
||||
.mainType(PluginType.GENERAL)
|
||||
.pluginIcon(R.drawable.open_humans_white)
|
||||
.pluginName(R.string.open_humans)
|
||||
.shortName(R.string.open_humans_short)
|
||||
.description(R.string.donate_your_data_to_science)
|
||||
.fragmentClass(OpenHumansFragment::class.qualifiedName)
|
||||
.preferencesId(R.xml.pref_openhumans),
|
||||
aapsLogger, resourceHelper, injector) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val OPEN_HUMANS_URL = "https://www.openhumans.org"
|
||||
private const val CLIENT_ID = "oie6DvnaEOagTxSoD6BukkLPwDhVr6cMlN74Ihz1"
|
||||
private const val CLIENT_SECRET = "jR0N8pkH1jOwtozHc7CsB1UPcJzFN95ldHcK4VGYIApecr8zGJox0v06xLwPLMASScngT12aIaIHXAVCJeKquEXAWG1XekZdbubSpccgNiQBmuVmIF8nc1xSKSNJltCf"
|
||||
private const val REDIRECT_URL = "androidaps://setup-openhumans"
|
||||
const val AUTH_URL = "https://www.openhumans.org/direct-sharing/projects/oauth2/authorize/?client_id=$CLIENT_ID&response_type=code"
|
||||
const val WORK_NAME = "Open Humans"
|
||||
const val NOTIFICATION_CHANNEL = "OpenHumans"
|
||||
private const val COPY_NOTIFICATION_ID = 3122
|
||||
private const val FAILURE_NOTIFICATION_ID = 3123
|
||||
private const val SUCCESS_NOTIFICATION_ID = 3124
|
||||
private const val SIGNED_OUT_NOTIFICATION_ID = 3125
|
||||
const val UPLOAD_NOTIFICATION_ID = 3126
|
||||
private const val UPLOAD_SEGMENT_SIZE = 10000L
|
||||
}
|
||||
|
||||
private val openHumansAPI = OpenHumansAPI(OPEN_HUMANS_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URL)
|
||||
|
||||
@Suppress("PrivatePropertyName")
|
||||
private val FILE_NAME_DATE_FORMAT = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
private var isSetup
|
||||
get() = sp.getBoolean("openhumans_is_setup", false)
|
||||
set(value) = sp.putBoolean("openhumans_is_setup", value)
|
||||
private var oAuthTokens: OpenHumansAPI.OAuthTokens?
|
||||
get() {
|
||||
return if (sp.contains("openhumans_access_token") && sp.contains("openhumans_refresh_token") && sp.contains("openhumans_expires_at")) {
|
||||
OpenHumansAPI.OAuthTokens(
|
||||
accessToken = sp.getStringOrNull("openhumans_access_token", null)!!,
|
||||
refreshToken = sp.getStringOrNull("openhumans_refresh_token", null)!!,
|
||||
expiresAt = sp.getLong("openhumans_expires_at", 0)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
if (value != null) {
|
||||
sp.putString("openhumans_access_token", value.accessToken)
|
||||
sp.putString("openhumans_refresh_token", value.refreshToken)
|
||||
sp.putLong("openhumans_expires_at", value.expiresAt)
|
||||
} else {
|
||||
sp.remove("openhumans_access_token")
|
||||
sp.remove("openhumans_refresh_token")
|
||||
sp.remove("openhumans_expires_at")
|
||||
sp.remove("openhumans_expires_at")
|
||||
}
|
||||
}
|
||||
var projectMemberId: String?
|
||||
get() = sp.getStringOrNull("openhumans_project_member_id", null)
|
||||
private set(value) {
|
||||
if (value == null) sp.remove("openhumans_project_member_id")
|
||||
else sp.putString("openhumans_project_member_id", value)
|
||||
}
|
||||
private var uploadCounter: Int
|
||||
get() = sp.getInt("openhumans_counter", 1)
|
||||
set(value) = sp.putInt("openhumans_counter", value)
|
||||
private val appId: UUID
|
||||
get() {
|
||||
val id = sp.getStringOrNull("openhumans_appid", null)
|
||||
return if (id == null) {
|
||||
val generated = UUID.randomUUID()
|
||||
sp.putString("openhumans_appid", generated.toString())
|
||||
generated
|
||||
} else {
|
||||
UUID.fromString(id)
|
||||
}
|
||||
}
|
||||
|
||||
private var copyDisposable: Disposable? = null
|
||||
|
||||
private val wakeLock = (context.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AndroidAPS::OpenHumans")
|
||||
|
||||
private val preferenceChangeDisposable = CompositeDisposable()
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setupNotificationChannel()
|
||||
if (isSetup) scheduleWorker(false)
|
||||
preferenceChangeDisposable += rxBus.toObservable(EventPreferenceChange::class.java).subscribe {
|
||||
onSharedPreferenceChanged(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
copyDisposable?.dispose()
|
||||
cancelWorker()
|
||||
preferenceChangeDisposable.clear()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
fun enqueueBGReading(glucoseValue: GlucoseValue?) = glucoseValue?.let {
|
||||
insertQueueItem("BgReadings") {
|
||||
put("date", glucoseValue.timestamp)
|
||||
put("isValid", glucoseValue.isValid)
|
||||
put("value", glucoseValue.value)
|
||||
put("direction", glucoseValue.trendArrow)
|
||||
put("raw", glucoseValue.raw)
|
||||
put("source", glucoseValue.sourceSensor)
|
||||
put("nsId", glucoseValue.interfaceIDs.nightscoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// @JvmOverloads
|
||||
// fun enqueueTreatment(treatment: Treatment?, deleted: Boolean = false) = treatment?.let {
|
||||
// insertQueueItem("Treatments") {
|
||||
// put("date", treatment.date)
|
||||
// put("isValid", treatment.isValid)
|
||||
// put("source", treatment.source)
|
||||
// put("nsId", treatment._id)
|
||||
// put("boluscalc", treatment.boluscalc)
|
||||
// put("carbs", treatment.carbs)
|
||||
// put("dia", treatment.dia)
|
||||
// put("insulin", treatment.insulin)
|
||||
// put("insulinInterfaceID", treatment.insulinInterfaceID)
|
||||
// put("isSMB", treatment.isSMB)
|
||||
// put("mealBolus", treatment.mealBolus)
|
||||
// put("bolusCalcJson", treatment.getBoluscalc())
|
||||
// put("isDeletion", deleted)
|
||||
// }
|
||||
// }
|
||||
|
||||
@JvmOverloads
|
||||
fun enqueueTherapyEvent(therapyEvent: TherapyEvent, deleted: Boolean = false) = insertQueueItem("TherapyEvents") {
|
||||
put("date", therapyEvent.timestamp)
|
||||
put("isValid", therapyEvent.isValid)
|
||||
put("nsId", therapyEvent.interfaceIDs.nightscoutId)
|
||||
put("eventType", therapyEvent.type.text)
|
||||
put("glucose", therapyEvent.glucose)
|
||||
put("units", therapyEvent.glucoseUnit.toConstant())
|
||||
put("glucoseType", therapyEvent.glucoseType?.text)
|
||||
put("duration", therapyEvent.duration)
|
||||
put("isDeletion", deleted)
|
||||
}
|
||||
|
||||
// @JvmOverloads
|
||||
// fun enqueueExtendedBolus(extendedBolus: ExtendedBolus, deleted: Boolean = false) = insertQueueItem("ExtendedBoluses") {
|
||||
// put("date", extendedBolus.date)
|
||||
// put("isValid", extendedBolus.isValid)
|
||||
// put("source", extendedBolus.source)
|
||||
// put("nsId", extendedBolus._id)
|
||||
// put("pumpId", extendedBolus.pumpId)
|
||||
// put("insulin", extendedBolus.insulin)
|
||||
// put("durationInMinutes", extendedBolus.durationInMinutes)
|
||||
// put("isDeletion", deleted)
|
||||
// }
|
||||
|
||||
// @JvmOverloads
|
||||
// fun enqueueProfileSwitch(profileSwitch: ProfileSwitch, deleted: Boolean = false) = insertQueueItem("ProfileSwitches") {
|
||||
// put("date", profileSwitch.date)
|
||||
// put("isValid", profileSwitch.isValid)
|
||||
// put("source", profileSwitch.source)
|
||||
// put("nsId", profileSwitch._id)
|
||||
// put("isCPP", profileSwitch.isCPP)
|
||||
// put("timeshift", profileSwitch.timeshift)
|
||||
// put("percentage", profileSwitch.percentage)
|
||||
// put("profile", JSONObject(profileSwitch.profileJson))
|
||||
// put("profilePlugin", profileSwitch.profilePlugin)
|
||||
// put("durationInMinutes", profileSwitch.durationInMinutes)
|
||||
// put("isDeletion", deleted)
|
||||
// }
|
||||
|
||||
// fun enqueueTotalDailyDose(tdd: TDD) = insertQueueItem("TotalDailyDoses") {
|
||||
// put("double", tdd.date)
|
||||
// put("double", tdd.bolus)
|
||||
// put("double", tdd.basal)
|
||||
// put("double", tdd.total)
|
||||
// }
|
||||
|
||||
// @JvmOverloads
|
||||
// fun enqueueTemporaryBasal(temporaryBasal: TemporaryBasal?, deleted: Boolean = false) = temporaryBasal?.let {
|
||||
// insertQueueItem("TemporaryBasals") {
|
||||
// put("date", temporaryBasal.date)
|
||||
// put("isValid", temporaryBasal.isValid)
|
||||
// put("source", temporaryBasal.source)
|
||||
// put("nsId", temporaryBasal._id)
|
||||
// put("pumpId", temporaryBasal.pumpId)
|
||||
// put("durationInMinutes", temporaryBasal.durationInMinutes)
|
||||
// put("durationInMinutes", temporaryBasal.durationInMinutes)
|
||||
// put("isAbsolute", temporaryBasal.isAbsolute)
|
||||
// put("percentRate", temporaryBasal.percentRate)
|
||||
// put("absoluteRate", temporaryBasal.absoluteRate)
|
||||
// put("isDeletion", deleted)
|
||||
// }
|
||||
// }
|
||||
|
||||
@JvmOverloads
|
||||
fun enqueueTempTarget(tempTarget: TemporaryTarget?, deleted: Boolean = false) = tempTarget?.let {
|
||||
insertQueueItem("TempTargets") {
|
||||
put("date", tempTarget.timestamp)
|
||||
put("isValid", tempTarget.isValid)
|
||||
put("nsId", tempTarget.interfaceIDs_backing?.nightscoutId)
|
||||
put("low", tempTarget.lowTarget)
|
||||
put("high", tempTarget.highTarget)
|
||||
put("reason", tempTarget.reason)
|
||||
put("durationInMinutes", tempTarget.duration)
|
||||
put("isDeletion", deleted)
|
||||
}
|
||||
}
|
||||
|
||||
fun enqueueSMBData(profile: JSONObject?, glucoseStatus: JSONObject?, iobData: JSONArray?, mealData: JSONObject?, currentTemp: JSONObject?, autosensData: JSONObject?, smbAllowed: Boolean, smbAlwaysAllowed: Boolean, result: JSONObject?) = insertQueueItem("APSData") {
|
||||
put("algorithm", "SMB")
|
||||
put("profile", profile)
|
||||
put("glucoseStatus", glucoseStatus)
|
||||
put("iobData", iobData)
|
||||
put("mealData", mealData)
|
||||
put("currentTemp", currentTemp)
|
||||
put("autosensData", autosensData)
|
||||
put("smbAllowed", smbAllowed)
|
||||
put("smbAlwaysAllowed", smbAlwaysAllowed)
|
||||
put("result", result)
|
||||
}
|
||||
|
||||
fun enqueueAMAData(profile: JSONObject?, glucoseStatus: JSONObject?, iobData: JSONArray?, mealData: JSONObject?, currentTemp: JSONObject?, autosensData: JSONObject?, result: JSONObject?) = insertQueueItem("APSData") {
|
||||
put("algorithm", "AMA")
|
||||
put("profile", profile)
|
||||
put("glucoseStatus", glucoseStatus)
|
||||
put("iobData", iobData)
|
||||
put("mealData", mealData)
|
||||
put("currentTemp", currentTemp)
|
||||
put("autosensData", autosensData)
|
||||
put("result", result)
|
||||
}
|
||||
|
||||
private fun insertQueueItem(file: String, structureVersion: Int = 1, generator: JSONObject.() -> Unit) {
|
||||
if (oAuthTokens != null && this.isEnabled(PluginType.GENERAL)) {
|
||||
try {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("structureVersion", structureVersion)
|
||||
jsonObject.put("queuedOn", System.currentTimeMillis())
|
||||
generator(jsonObject)
|
||||
// val queueItem = OHQueueItem(
|
||||
// file = file,
|
||||
// content = jsonObject.toString()
|
||||
// )
|
||||
// databaseHelper.createOrUpdate(queueItem)
|
||||
rxBus.send(OpenHumansFragment.UpdateQueueEvent)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(authCode: String): Completable =
|
||||
openHumansAPI.exchangeAuthToken(authCode)
|
||||
.doOnSuccess {
|
||||
oAuthTokens = it
|
||||
}
|
||||
.flatMap { openHumansAPI.getProjectMemberId(it.accessToken) }
|
||||
.doOnSuccess {
|
||||
projectMemberId = it
|
||||
copyExistingDataToQueue()
|
||||
rxBus.send(OpenHumansFragment.UpdateViewEvent)
|
||||
}
|
||||
.doOnError {
|
||||
aapsLogger.error("Failed to login to Open Humans", it)
|
||||
}
|
||||
.ignoreElement()
|
||||
|
||||
fun logout() {
|
||||
cancelWorker()
|
||||
copyDisposable?.dispose()
|
||||
isSetup = false
|
||||
oAuthTokens = null
|
||||
projectMemberId = null
|
||||
// databaseHelper.clearOpenHumansQueue()
|
||||
rxBus.send(OpenHumansFragment.UpdateViewEvent)
|
||||
}
|
||||
|
||||
private fun copyExistingDataToQueue() {
|
||||
copyDisposable?.dispose()
|
||||
var currentProgress = 0L
|
||||
var maxProgress = 0L
|
||||
val increaseCounter = {
|
||||
currentProgress++
|
||||
//Updating the notification for every item drastically slows down the operation
|
||||
if (currentProgress % 1000L == 0L) showOngoingNotification(maxProgress, currentProgress)
|
||||
}
|
||||
// copyDisposable = Completable.fromCallable { databaseHelper.clearOpenHumansQueue() }
|
||||
// .andThen(Single.defer { Single.just(databaseHelper.getCountOfAllRows() + treatmentsPlugin.service.count()) })
|
||||
// .doOnSuccess { maxProgress = it }
|
||||
// .flatMapObservable { Observable.defer { Observable.fromIterable(treatmentsPlugin.service.getTreatmentData()) } }
|
||||
// .map { enqueueTreatment(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .andThen(Observable.defer { Observable.fromIterable(repository.compatGetBgReadingsDataFromTime(0, true).blockingGet()) })
|
||||
// .map { enqueueBGReading(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .andThen(Observable.defer { Observable.fromIterable(repository.compatGetTherapyEventDataFromTime(0, true).blockingGet()) })
|
||||
// .map { enqueueTherapyEvent(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .andThen(Observable.defer { Observable.fromIterable(databaseHelper.getAllExtendedBoluses()) })
|
||||
// .map { enqueueExtendedBolus(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .andThen(Observable.defer { Observable.fromIterable(databaseHelper.getAllProfileSwitches()) })
|
||||
// .map { enqueueProfileSwitch(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .andThen(Observable.defer { Observable.fromIterable(databaseHelper.getAllTDDs()) })
|
||||
// .map { enqueueTotalDailyDose(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .andThen(Observable.defer { Observable.fromIterable(databaseHelper.getAllTemporaryBasals()) })
|
||||
// .map { enqueueTemporaryBasal(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .andThen(Observable.defer { Observable.fromIterable(repository.compatGetTemporaryTargetData().blockingGet()) })
|
||||
// .map { enqueueTempTarget(it); increaseCounter() }
|
||||
// .ignoreElements()
|
||||
// .doOnSubscribe {
|
||||
// wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
||||
// showOngoingNotification()
|
||||
// }
|
||||
// .doOnComplete {
|
||||
// isSetup = true
|
||||
// scheduleWorker(false)
|
||||
// showSetupFinishedNotification()
|
||||
// }
|
||||
// .doOnError {
|
||||
// logout()
|
||||
// showSetupFailedNotification()
|
||||
// }
|
||||
// .doFinally {
|
||||
// copyDisposable = null
|
||||
// NotificationManagerCompat.from(context).cancel(COPY_NOTIFICATION_ID)
|
||||
// wakeLock.release()
|
||||
// }
|
||||
// .onErrorComplete()
|
||||
// .subscribeOn(aapsSchedulers.io)
|
||||
// .subscribe()
|
||||
}
|
||||
|
||||
private fun showOngoingNotification(maxProgress: Long? = null, currentProgress: Long? = null) {
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||
.setContentTitle(resourceHelper.gs(R.string.finishing_open_humans_setup))
|
||||
.setContentText(resourceHelper.gs(R.string.this_may_take_a_while))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setProgress(maxProgress?.toInt() ?: 0, currentProgress?.toInt()
|
||||
?: 0, maxProgress == null || currentProgress == null)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.build()
|
||||
NotificationManagerCompat.from(context).notify(COPY_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showSetupFinishedNotification() {
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||
.setContentTitle(resourceHelper.gs(R.string.setup_finished))
|
||||
.setContentText(resourceHelper.gs(R.string.your_phone_will_upload_data))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.build()
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
notificationManager.notify(SUCCESS_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showSetupFailedNotification() {
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||
.setContentTitle(resourceHelper.gs(R.string.setup_failed))
|
||||
.setContentText(resourceHelper.gs(R.string.there_was_an_error))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.build()
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
notificationManager.notify(FAILURE_NOTIFICATION_ID, notification)
|
||||
}
|
||||
/*
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
fun uploadDataSegmentally(): Completable =
|
||||
uploadData(UPLOAD_SEGMENT_SIZE)
|
||||
.repeatUntil { databaseHelper.getOHQueueSize() == 0L }
|
||||
.doOnSubscribe {
|
||||
aapsLogger.info(LTag.OHUPLOADER, "Starting segmental upload")
|
||||
}
|
||||
.doOnComplete {
|
||||
aapsLogger.info(LTag.OHUPLOADER, "Segmental upload successful")
|
||||
}
|
||||
.doOnError {
|
||||
aapsLogger.error(LTag.OHUPLOADER, "Segmental upload exceptional", it)
|
||||
}
|
||||
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
@Suppress("SameParameterValue")
|
||||
private fun uploadData(maxEntries: Long): Completable = gatherData(maxEntries)
|
||||
.flatMap { data -> refreshAccessTokensIfNeeded().map { accessToken -> accessToken to data } }
|
||||
.flatMap { uploadFile(it.first, it.second).andThen(Single.just(it.second)) }
|
||||
.flatMapCompletable {
|
||||
if (it.highestQueueId != null) {
|
||||
removeUploadedEntriesFromQueue(it.highestQueueId)
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
}
|
||||
.doOnError {
|
||||
if (it is OpenHumansAPI.OHHttpException && it.code == 401 && it.detail == "Invalid token.") {
|
||||
handleSignOut()
|
||||
}
|
||||
aapsLogger.error("Error while uploading to Open Humans", it)
|
||||
}
|
||||
.doOnComplete {
|
||||
aapsLogger.info(LTag.OHUPLOADER, "Upload successful")
|
||||
rxBus.send(OpenHumansFragment.UpdateQueueEvent)
|
||||
}
|
||||
.doOnSubscribe {
|
||||
aapsLogger.info(LTag.OHUPLOADER, "Starting upload")
|
||||
}
|
||||
*/
|
||||
private fun uploadFile(accessToken: String, uploadData: UploadData) = Completable.defer {
|
||||
openHumansAPI.prepareFileUpload(accessToken, uploadData.fileName, uploadData.metadata)
|
||||
.flatMap { openHumansAPI.uploadFile(it.uploadURL, uploadData.content).andThen(Single.just(it.fileId)) }
|
||||
.flatMapCompletable { openHumansAPI.completeFileUpload(accessToken, it) }
|
||||
}
|
||||
|
||||
private fun refreshAccessTokensIfNeeded() = Single.defer {
|
||||
val oAuthTokens = this.oAuthTokens!!
|
||||
if (oAuthTokens.expiresAt <= System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)) {
|
||||
openHumansAPI.refreshAccessToken(oAuthTokens.refreshToken)
|
||||
.doOnSuccess { this.oAuthTokens = it }
|
||||
.map { it.accessToken }
|
||||
} else {
|
||||
Single.just(oAuthTokens.accessToken)
|
||||
}
|
||||
}
|
||||
/*
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
private fun gatherData(maxEntries: Long) = Single.defer {
|
||||
val items = databaseHelper.getAllOHQueueItems(maxEntries)
|
||||
val baos = ByteArrayOutputStream()
|
||||
val zos = ZipOutputStream(baos)
|
||||
val tags = mutableListOf<String>()
|
||||
|
||||
items.groupBy { it.file }.forEach { entry ->
|
||||
tags.add(entry.key)
|
||||
val jsonArray = JSONArray()
|
||||
entry.value.map { it.content }.forEach { jsonArray.put(JSONObject(it)) }
|
||||
zos.writeFile("${entry.key}.json", jsonArray.toString().toByteArray())
|
||||
}
|
||||
|
||||
val applicationInfo = JSONObject()
|
||||
applicationInfo.put("versionName", BuildConfig.VERSION_NAME)
|
||||
applicationInfo.put("versionCode", BuildConfig.VERSION_CODE)
|
||||
val hasGitInfo = !BuildConfig.HEAD.endsWith("NoGitSystemAvailable", true)
|
||||
val customRemote = !BuildConfig.REMOTE.equals("https://github.com/nightscout/AndroidAPS.git", true)
|
||||
applicationInfo.put("hasGitInfo", hasGitInfo)
|
||||
applicationInfo.put("customRemote", customRemote)
|
||||
applicationInfo.put("applicationId", appId.toString())
|
||||
zos.writeFile("ApplicationInfo.json", applicationInfo.toString().toByteArray())
|
||||
tags.add("ApplicationInfo")
|
||||
|
||||
val preferences = JSONObject(sp.getAll().filterKeys { it.isAllowedKey() })
|
||||
zos.writeFile("Preferences.json", preferences.toString().toByteArray())
|
||||
tags.add("Preferences")
|
||||
|
||||
val deviceInfo = JSONObject()
|
||||
deviceInfo.put("brand", Build.BRAND)
|
||||
deviceInfo.put("device", Build.DEVICE)
|
||||
deviceInfo.put("manufacturer", Build.MANUFACTURER)
|
||||
deviceInfo.put("model", Build.MODEL)
|
||||
deviceInfo.put("product", Build.PRODUCT)
|
||||
zos.writeFile("DeviceInfo.json", deviceInfo.toString().toByteArray())
|
||||
tags.add("DeviceInfo")
|
||||
|
||||
val displayMetrics = DisplayMetrics()
|
||||
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(displayMetrics)
|
||||
val displayInfo = JSONObject()
|
||||
displayInfo.put("height", displayMetrics.heightPixels)
|
||||
displayInfo.put("width", displayMetrics.widthPixels)
|
||||
displayInfo.put("density", displayMetrics.density)
|
||||
displayInfo.put("scaledDensity", displayMetrics.scaledDensity)
|
||||
displayInfo.put("xdpi", displayMetrics.xdpi)
|
||||
displayInfo.put("ydpi", displayMetrics.ydpi)
|
||||
zos.writeFile("DisplayInfo.json", displayInfo.toString().toByteArray())
|
||||
tags.add("DisplayInfo")
|
||||
|
||||
val uploadNumber = this.uploadCounter++
|
||||
val uploadDate = Date()
|
||||
val uploadInfo = JSONObject()
|
||||
uploadInfo.put("fileVersion", 1)
|
||||
uploadInfo.put("counter", uploadNumber)
|
||||
uploadInfo.put("timestamp", uploadDate.time)
|
||||
uploadInfo.put("utcOffset", TimeZone.getDefault().getOffset(uploadDate.time))
|
||||
zos.writeFile("UploadInfo.json", uploadInfo.toString().toByteArray())
|
||||
tags.add("UploadInfo")
|
||||
|
||||
zos.close()
|
||||
val bytes = baos.toByteArray()
|
||||
|
||||
Single.just(UploadData(
|
||||
fileName = "upload-num$uploadNumber-ver1-date${FILE_NAME_DATE_FORMAT.format(uploadDate)}-appid${appId.toString().replace("-", "")}.zip",
|
||||
metadata = OpenHumansAPI.FileMetadata(
|
||||
tags = tags,
|
||||
description = "AndroidAPS Database Upload",
|
||||
md5 = MessageDigest.getInstance("MD5").digest(bytes).toHexString(),
|
||||
creationDate = uploadDate.time
|
||||
),
|
||||
content = bytes,
|
||||
highestQueueId = items.map { it.id }.maxOrNull()
|
||||
))
|
||||
}
|
||||
*/
|
||||
private fun ZipOutputStream.writeFile(name: String, bytes: ByteArray) {
|
||||
putNextEntry(ZipEntry(name))
|
||||
write(bytes)
|
||||
closeEntry()
|
||||
}
|
||||
|
||||
private fun removeUploadedEntriesFromQueue(highestId: Long) = Completable.fromCallable {
|
||||
// databaseHelper.removeAllOHQueueItemsWithIdSmallerThan(highestId)
|
||||
}
|
||||
|
||||
private fun handleSignOut() {
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||
.setContentTitle(resourceHelper.gs(R.string.you_have_been_signed_out_of_open_humans))
|
||||
.setContentText(resourceHelper.gs(R.string.click_here_to_sign_in_again_if_this_wasnt_on_purpose))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, OpenHumansLoginActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
},
|
||||
0
|
||||
))
|
||||
.build()
|
||||
NotificationManagerCompat.from(context).notify(SIGNED_OUT_NOTIFICATION_ID, notification)
|
||||
logout()
|
||||
}
|
||||
|
||||
private fun cancelWorker() {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
|
||||
}
|
||||
|
||||
private fun scheduleWorker(replace: Boolean) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiresCharging(sp.getBoolean("key_oh_charging_only", false))
|
||||
.build()
|
||||
val workRequest = PeriodicWorkRequestBuilder<OHUploadWorker>(1, TimeUnit.DAYS)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 20, TimeUnit.MINUTES)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_NAME, if (replace) ExistingPeriodicWorkPolicy.REPLACE else ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
||||
}
|
||||
|
||||
private fun setupNotificationChannel() {
|
||||
val notificationManagerCompat = NotificationManagerCompat.from(context)
|
||||
notificationManagerCompat.createNotificationChannel(NotificationChannel(
|
||||
NOTIFICATION_CHANNEL,
|
||||
resourceHelper.gs(R.string.open_humans),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
))
|
||||
}
|
||||
|
||||
private class UploadData(
|
||||
val fileName: String,
|
||||
val metadata: OpenHumansAPI.FileMetadata,
|
||||
val content: ByteArray,
|
||||
val highestQueueId: Long?
|
||||
)
|
||||
|
||||
@Suppress("PrivatePropertyName")
|
||||
private val HEX_DIGITS = "0123456789ABCDEF".toCharArray()
|
||||
|
||||
private fun ByteArray.toHexString(): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
map { it.toInt() }.forEach {
|
||||
stringBuilder.append(HEX_DIGITS[(it shr 4) and 0x0F])
|
||||
stringBuilder.append(HEX_DIGITS[it and 0x0F])
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
private fun onSharedPreferenceChanged(event: EventPreferenceChange) {
|
||||
if (event.changedKey == "key_oh_charging_only" && isSetup) scheduleWorker(true)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector android:height="100.5272dp" android:viewportHeight="108.36626"
|
||||
android:viewportWidth="107.79796" android:width="100dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ff9161" android:pathData="m59.75,62.316c5.179,-2.256 8.801,-7.417 8.801,-13.427 0,-8.086 -6.555,-14.641 -14.641,-14.641 -8.086,0 -14.641,6.555 -14.641,14.641 0,6.01 3.622,11.171 8.801,13.427 -7.849,1.589 -14.555,6.318 -18.76,12.817 5.968,6.896 14.774,11.272 24.589,11.272 9.821,0 18.633,-4.382 24.601,-11.286 -4.205,-6.491 -10.907,-11.215 -18.75,-12.803z"/>
|
||||
<path android:fillColor="#ff9161" android:pathData="M21.689,33.33 L10.002,21.643c-5.155,7 -8.677,15.271 -10.002,24.25l16.523,0c0.968,-4.535 2.741,-8.776 5.166,-12.563z"/>
|
||||
<path android:fillColor="#ff9161" android:pathData="m91.275,45.893l16.523,0C106.473,36.909 102.947,28.634 97.787,21.631L86.101,33.317c2.429,3.79 4.205,8.035 5.174,12.576z"/>
|
||||
<path android:fillColor="#ff9161" android:pathData="M86.305,10.106C79.304,4.91 71.02,1.351 62.022,0l0,15.422l13.059,5.908z"/>
|
||||
<path android:fillColor="#ff9161" android:pathData="M45.754,15.339L45.754,0.003c-8.995,1.354 -17.276,4.915 -24.274,10.113l10.963,10.963z"/>
|
||||
<path android:fillColor="#4bc0c7" android:pathData="m26.558,80.554c-4.881,-5.002 -8.405,-11.333 -9.971,-18.394l-16.546,0c4.001,26.128 26.629,46.206 53.858,46.206 27.229,0 49.857,-20.077 53.858,-46.206l-16.546,0c-1.564,7.053 -5.082,13.378 -9.955,18.378 -6.946,7.127 -16.643,11.56 -27.357,11.56 -10.706,0 -20.396,-4.427 -27.341,-11.544z"/>
|
||||
</vector>
|
|
@ -1,58 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/uploaded_data"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/the_following_data_will_be_uploaded_to_your_open_humans_account"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/terms_of_use"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/open_humans_terms"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/i_understand_and_agree" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
|
@ -29,7 +29,6 @@
|
|||
|
||||
<string name="key_email_for_crash_report" translatable="false">email_for_crash_report</string>
|
||||
<string name="key_smscommunicator_settings" translatable="false">smscommunicator</string>
|
||||
<string name="key_open_humans_settings" translatable="false">open_humans</string>
|
||||
<string name="key_protection_settings" translatable="false">protection</string>
|
||||
<string name="key_absorption_category_settings" translatable="false">absorption_category_settings</string>
|
||||
<string name="key_insulin_oref_peak_settings" translatable="false">insulin_oref_peak_settings</string>
|
||||
|
@ -1055,36 +1054,6 @@
|
|||
<string name="basalpctfromtdd_label">% of basal</string>
|
||||
<string name="dpvdefaultprofile">DPV Default profile</string>
|
||||
|
||||
<string name="open_humans">Open Humans</string>
|
||||
<string name="finishing_open_humans_setup">Finishing Open Humans setup…</string>
|
||||
<string name="this_may_take_a_while">This may take a while. Do not turn your phone or this plugin off.</string>
|
||||
<string name="setup_finished">Setup finished</string>
|
||||
<string name="your_phone_will_upload_data">Your phone will upload data to Open Humans soon.</string>
|
||||
<string name="your_phone_is_upload_data">Your phone is uploading data to Open Humans now.</string>
|
||||
<string name="setup_failed">Setup failed</string>
|
||||
<string name="there_was_an_error">There was an error. Please try to log in again in order to proceed. Sorry & Thank you!</string>
|
||||
<string name="open_humans_terms">This is an open source tool that will copy your data to Open Humans. We retain no rights to share your data with third parties without your explicit authorization. The data the project and app receive are identified via a random user ID and will only be securely transmitted to an Open Humans account with your authorization of that process. You can stop uploading and delete your upload data at any time via www.openhumans.org.</string>
|
||||
<string name="i_understand_and_agree">I understand and agree.</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="logout">Logout</string>
|
||||
<string name="oh_logout_confirmation">Do you really want to log out and stop donating data to science?</string>
|
||||
<string name="project_member_id">Project Member ID: %s</string>
|
||||
<string name="queue_size">Queue Size: %d</string>
|
||||
<string name="terms_of_use">Terms of Use</string>
|
||||
<string name="not_logged_in">Not logged in</string>
|
||||
<string name="you_need_to_accept_the_of_use_first">You need to accept the terms of use first.</string>
|
||||
<string name="successfully_logged_in">Successfully logged in</string>
|
||||
<string name="setup_will_continue_in_background">The setup will be completed in background now. Thanks for uploading your data.\n\nPlease keep this plugin and your phone turned on for a short while for the setup to complete.</string>
|
||||
<string name="completing_login">Completing login…</string>
|
||||
<string name="donate_your_data_to_science">Donate your data to science</string>
|
||||
<string name="open_humans_short">OH</string>
|
||||
<string name="you_have_been_signed_out_of_open_humans">You have been signed out of Open Humans</string>
|
||||
<string name="click_here_to_sign_in_again_if_this_wasnt_on_purpose">Click here to sign in again if this wasn\'t on purpose.</string>
|
||||
<string name="only_upload_if_connected_to_wifi">Only upload if connected to WiFi</string>
|
||||
<string name="only_upload_if_charging">Only upload if charging</string>
|
||||
<string name="worker_state">Worker State: %s</string>
|
||||
<string name="uploaded_data">Uploaded Data</string>
|
||||
<string name="the_following_data_will_be_uploaded_to_your_open_humans_account">The following data will be uploaded to your Open Humans account: Glucose values, boluses, carbs, careportal events (except notes), extended boluses, profile switches, total daily doses, temporary basals, temp targets, preferences, application version, device model and screen dimensions. Secret or private information such as your Nightscout URL or API secret will not be uploaded.</string>
|
||||
<string name="setupwizard_pump_riley_link_status">RileyLink status:</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="copytolocalprofile_invalid">Unable to create local profile. Profile is invalid.</string>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package info.nightscout.androidaps.database
|
||||
|
||||
import info.nightscout.androidaps.database.data.NewEntries
|
||||
import info.nightscout.androidaps.database.entities.*
|
||||
import info.nightscout.androidaps.database.interfaces.DBEntry
|
||||
import info.nightscout.androidaps.database.transactions.Transaction
|
||||
|
@ -830,7 +831,25 @@ open class AppRepository @Inject internal constructor(
|
|||
.subscribeOn(Schedulers.io())
|
||||
.toWrappedSingle()
|
||||
|
||||
|
||||
suspend fun collectNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int) = NewEntries(
|
||||
apsResults = database.apsResultDao.getNewEntriesSince(since, until, limit, offset),
|
||||
apsResultLinks = database.apsResultLinkDao.getNewEntriesSince(since, until, limit, offset),
|
||||
bolusCalculatorResults = database.bolusCalculatorResultDao.getNewEntriesSince(since, until, limit, offset),
|
||||
boluses = database.bolusDao.getNewEntriesSince(since, until, limit, offset),
|
||||
carbs = database.carbsDao.getNewEntriesSince(since, until, limit, offset),
|
||||
effectiveProfileSwitches = database.effectiveProfileSwitchDao.getNewEntriesSince(since, until, limit, offset),
|
||||
extendedBoluses = database.extendedBolusDao.getNewEntriesSince(since, until, limit, offset),
|
||||
glucoseValues = database.glucoseValueDao.getNewEntriesSince(since, until, limit, offset),
|
||||
multiwaveBolusLinks = database.multiwaveBolusLinkDao.getNewEntriesSince(since, until, limit, offset),
|
||||
offlineEvents = database.offlineEventDao.getNewEntriesSince(since, until, limit, offset),
|
||||
preferencesChanges = database.preferenceChangeDao.getNewEntriesSince(since, until, limit, offset),
|
||||
profileSwitches = database.profileSwitchDao.getNewEntriesSince(since, until, limit, offset),
|
||||
temporaryBasals = database.temporaryBasalDao.getNewEntriesSince(since, until, limit, offset),
|
||||
temporaryTarget = database.temporaryTargetDao.getNewEntriesSince(since, until, limit, offset),
|
||||
therapyEvents = database.therapyEventDao.getNewEntriesSince(since, until, limit, offset),
|
||||
totalDailyDoses = database.totalDailyDoseDao.getNewEntriesSince(since, until, limit, offset),
|
||||
versionChanges = database.versionChangeDao.getNewEntriesSince(since, until, limit, offset),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("USELESS_CAST")
|
||||
|
|
|
@ -4,7 +4,6 @@ import androidx.room.Dao
|
|||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_APS_RESULTS
|
||||
import info.nightscout.androidaps.database.entities.APSResult
|
||||
import io.reactivex.Single
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@Dao
|
||||
|
@ -15,4 +14,7 @@ internal interface APSResultDao : TraceableDao<APSResult> {
|
|||
|
||||
@Query("DELETE FROM $TABLE_APS_RESULTS")
|
||||
override fun deleteAllEntries()
|
||||
|
||||
@Query("SELECT * FROM $TABLE_APS_RESULTS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<APSResult>
|
||||
}
|
|
@ -5,7 +5,6 @@ import androidx.room.Query
|
|||
import info.nightscout.androidaps.database.TABLE_APS_RESULTS
|
||||
import info.nightscout.androidaps.database.TABLE_APS_RESULT_LINKS
|
||||
import info.nightscout.androidaps.database.entities.APSResultLink
|
||||
import io.reactivex.Single
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@Dao
|
||||
|
@ -16,4 +15,7 @@ internal interface APSResultLinkDao : TraceableDao<APSResultLink> {
|
|||
|
||||
@Query("DELETE FROM $TABLE_APS_RESULTS")
|
||||
override fun deleteAllEntries()
|
||||
|
||||
@Query("SELECT * FROM $TABLE_APS_RESULT_LINKS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<APSResultLink>
|
||||
}
|
|
@ -2,6 +2,7 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_APS_RESULTS
|
||||
import info.nightscout.androidaps.database.TABLE_BOLUS_CALCULATOR_RESULTS
|
||||
import info.nightscout.androidaps.database.entities.BolusCalculatorResult
|
||||
import io.reactivex.Maybe
|
||||
|
@ -37,4 +38,7 @@ internal interface BolusCalculatorResultDao : TraceableDao<BolusCalculatorResult
|
|||
|
||||
@Query("SELECT * FROM $TABLE_BOLUS_CALCULATOR_RESULTS WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<BolusCalculatorResult>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_BOLUS_CALCULATOR_RESULTS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<BolusCalculatorResult>
|
||||
}
|
|
@ -3,8 +3,10 @@ package info.nightscout.androidaps.database.daos
|
|||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_BOLUSES
|
||||
import info.nightscout.androidaps.database.TABLE_BOLUS_CALCULATOR_RESULTS
|
||||
import info.nightscout.androidaps.database.embedments.InterfaceIDs
|
||||
import info.nightscout.androidaps.database.entities.Bolus
|
||||
import info.nightscout.androidaps.database.entities.BolusCalculatorResult
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
||||
|
@ -67,4 +69,7 @@ internal interface BolusDao : TraceableDao<Bolus> {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_BOLUSES WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<Bolus>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_BOLUSES WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<Bolus>
|
||||
}
|
|
@ -2,6 +2,7 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_BOLUSES
|
||||
import info.nightscout.androidaps.database.TABLE_CARBS
|
||||
import info.nightscout.androidaps.database.entities.Carbs
|
||||
import io.reactivex.Maybe
|
||||
|
@ -69,4 +70,7 @@ internal interface CarbsDao : TraceableDao<Carbs> {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_CARBS WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<Carbs>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_CARBS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<Carbs>
|
||||
}
|
|
@ -2,7 +2,9 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_CARBS
|
||||
import info.nightscout.androidaps.database.TABLE_EFFECTIVE_PROFILE_SWITCHES
|
||||
import info.nightscout.androidaps.database.entities.Carbs
|
||||
import info.nightscout.androidaps.database.entities.EffectiveProfileSwitch
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -49,4 +51,7 @@ internal interface EffectiveProfileSwitchDao : TraceableDao<EffectiveProfileSwit
|
|||
@Query("SELECT * FROM $TABLE_EFFECTIVE_PROFILE_SWITCHES WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<EffectiveProfileSwitch>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_EFFECTIVE_PROFILE_SWITCHES WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<EffectiveProfileSwitch>
|
||||
|
||||
}
|
|
@ -2,8 +2,10 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_CARBS
|
||||
import info.nightscout.androidaps.database.TABLE_EXTENDED_BOLUSES
|
||||
import info.nightscout.androidaps.database.embedments.InterfaceIDs
|
||||
import info.nightscout.androidaps.database.entities.Carbs
|
||||
import info.nightscout.androidaps.database.entities.ExtendedBolus
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -65,4 +67,7 @@ internal interface ExtendedBolusDao : TraceableDao<ExtendedBolus> {
|
|||
@Query("SELECT * FROM $TABLE_EXTENDED_BOLUSES WHERE isValid = 1 AND referenceId IS NULL ORDER BY id ASC LIMIT 1")
|
||||
fun getOldestRecord(): ExtendedBolus?
|
||||
|
||||
@Query("SELECT * FROM $TABLE_EXTENDED_BOLUSES WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<ExtendedBolus>
|
||||
|
||||
}
|
|
@ -2,7 +2,9 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_CARBS
|
||||
import info.nightscout.androidaps.database.TABLE_GLUCOSE_VALUES
|
||||
import info.nightscout.androidaps.database.entities.Carbs
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -53,4 +55,7 @@ internal interface GlucoseValueDao : TraceableDao<GlucoseValue> {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_GLUCOSE_VALUES WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<GlucoseValue>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_GLUCOSE_VALUES WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<GlucoseValue>
|
||||
}
|
|
@ -2,7 +2,9 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_GLUCOSE_VALUES
|
||||
import info.nightscout.androidaps.database.TABLE_MULTIWAVE_BOLUS_LINKS
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.database.entities.MultiwaveBolusLink
|
||||
|
||||
@Suppress("FunctionName")
|
||||
|
@ -14,4 +16,7 @@ internal interface MultiwaveBolusLinkDao : TraceableDao<MultiwaveBolusLink> {
|
|||
|
||||
@Query("DELETE FROM $TABLE_MULTIWAVE_BOLUS_LINKS")
|
||||
override fun deleteAllEntries()
|
||||
|
||||
@Query("SELECT * FROM $TABLE_MULTIWAVE_BOLUS_LINKS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<MultiwaveBolusLink>
|
||||
}
|
|
@ -2,7 +2,9 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_GLUCOSE_VALUES
|
||||
import info.nightscout.androidaps.database.TABLE_OFFLINE_EVENTS
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.database.entities.OfflineEvent
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -48,4 +50,7 @@ internal interface OfflineEventDao : TraceableDao<OfflineEvent> {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_OFFLINE_EVENTS WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<OfflineEvent>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_OFFLINE_EVENTS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<OfflineEvent>
|
||||
}
|
|
@ -3,7 +3,9 @@ package info.nightscout.androidaps.database.daos
|
|||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_GLUCOSE_VALUES
|
||||
import info.nightscout.androidaps.database.TABLE_PREFERENCE_CHANGES
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.database.entities.PreferenceChange
|
||||
import io.reactivex.Single
|
||||
|
||||
|
@ -13,4 +15,7 @@ interface PreferenceChangeDao {
|
|||
@Insert
|
||||
fun insert(preferenceChange: PreferenceChange)
|
||||
|
||||
@Query("SELECT * FROM $TABLE_PREFERENCE_CHANGES WHERE timestamp > :since AND timestamp <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<PreferenceChange>
|
||||
|
||||
}
|
|
@ -2,9 +2,11 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_GLUCOSE_VALUES
|
||||
import info.nightscout.androidaps.database.TABLE_PROFILE_SWITCHES
|
||||
import info.nightscout.androidaps.database.daos.workaround.ProfileSwitchDaoWorkaround
|
||||
import info.nightscout.androidaps.database.data.checkSanity
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.database.entities.ProfileSwitch
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -53,6 +55,9 @@ internal interface ProfileSwitchDao : ProfileSwitchDaoWorkaround {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_PROFILE_SWITCHES WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<ProfileSwitch>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_PROFILE_SWITCHES WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<ProfileSwitch>
|
||||
}
|
||||
|
||||
internal fun ProfileSwitchDao.insertNewEntryImpl(entry: ProfileSwitch): Long {
|
||||
|
|
|
@ -2,8 +2,10 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_GLUCOSE_VALUES
|
||||
import info.nightscout.androidaps.database.TABLE_TEMPORARY_BASALS
|
||||
import info.nightscout.androidaps.database.embedments.InterfaceIDs
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.database.entities.TemporaryBasal
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -76,4 +78,7 @@ internal interface TemporaryBasalDao : TraceableDao<TemporaryBasal> {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_TEMPORARY_BASALS WHERE isValid = 1 AND referenceId IS NULL ORDER BY id ASC LIMIT 1")
|
||||
fun getOldestRecord(): TemporaryBasal?
|
||||
|
||||
@Query("SELECT * FROM $TABLE_TEMPORARY_BASALS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<TemporaryBasal>
|
||||
}
|
|
@ -2,7 +2,9 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_GLUCOSE_VALUES
|
||||
import info.nightscout.androidaps.database.TABLE_TEMPORARY_TARGETS
|
||||
import info.nightscout.androidaps.database.entities.GlucoseValue
|
||||
import info.nightscout.androidaps.database.entities.TemporaryTarget
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -45,4 +47,7 @@ internal interface TemporaryTargetDao : TraceableDao<TemporaryTarget> {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_TEMPORARY_TARGETS WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<TemporaryTarget>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_TEMPORARY_TARGETS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<TemporaryTarget>
|
||||
}
|
|
@ -2,7 +2,9 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_TEMPORARY_TARGETS
|
||||
import info.nightscout.androidaps.database.TABLE_THERAPY_EVENTS
|
||||
import info.nightscout.androidaps.database.entities.TemporaryTarget
|
||||
import info.nightscout.androidaps.database.entities.TherapyEvent
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
|
@ -56,4 +58,7 @@ internal interface TherapyEventDao : TraceableDao<TherapyEvent> {
|
|||
|
||||
@Query("SELECT * FROM $TABLE_THERAPY_EVENTS WHERE id = :referenceId")
|
||||
fun getCurrentFromHistoric(referenceId: Long): Maybe<TherapyEvent>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_THERAPY_EVENTS WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<TherapyEvent>
|
||||
}
|
|
@ -2,8 +2,10 @@ package info.nightscout.androidaps.database.daos
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_TEMPORARY_TARGETS
|
||||
import info.nightscout.androidaps.database.TABLE_TOTAL_DAILY_DOSES
|
||||
import info.nightscout.androidaps.database.embedments.InterfaceIDs
|
||||
import info.nightscout.androidaps.database.entities.TemporaryTarget
|
||||
import info.nightscout.androidaps.database.entities.TotalDailyDose
|
||||
import io.reactivex.Single
|
||||
|
||||
|
@ -29,4 +31,7 @@ internal interface TotalDailyDoseDao : TraceableDao<TotalDailyDose> {
|
|||
@Query("SELECT * FROM $TABLE_TOTAL_DAILY_DOSES WHERE isValid = 1 AND referenceId IS NULL ORDER BY timestamp DESC LIMIT :count")
|
||||
fun getLastTotalDailyDoses(count: Int): Single<List<TotalDailyDose>>
|
||||
|
||||
@Query("SELECT * FROM $TABLE_TOTAL_DAILY_DOSES WHERE dateCreated > :since AND dateCreated <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<TotalDailyDose>
|
||||
|
||||
}
|
|
@ -3,7 +3,9 @@ package info.nightscout.androidaps.database.daos
|
|||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import info.nightscout.androidaps.database.TABLE_TEMPORARY_TARGETS
|
||||
import info.nightscout.androidaps.database.TABLE_VERSION_CHANGES
|
||||
import info.nightscout.androidaps.database.entities.TemporaryTarget
|
||||
import info.nightscout.androidaps.database.entities.VersionChange
|
||||
import io.reactivex.Single
|
||||
|
||||
|
@ -16,4 +18,7 @@ interface VersionChangeDao {
|
|||
@Query("SELECT * FROM $TABLE_VERSION_CHANGES ORDER BY id DESC LIMIT 1")
|
||||
fun getMostRecentVersionChange(): VersionChange?
|
||||
|
||||
@Query("SELECT * FROM $TABLE_VERSION_CHANGES WHERE timestamp > :since AND timestamp <= :until LIMIT :limit OFFSET :offset")
|
||||
suspend fun getNewEntriesSince(since: Long, until: Long, limit: Int, offset: Int): List<VersionChange>
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package info.nightscout.androidaps.database.data
|
||||
|
||||
import info.nightscout.androidaps.database.entities.*
|
||||
|
||||
data class NewEntries(
|
||||
val apsResults: List<APSResult>,
|
||||
val apsResultLinks: List<APSResultLink>,
|
||||
val bolusCalculatorResults: List<BolusCalculatorResult>,
|
||||
val boluses: List<Bolus>,
|
||||
val carbs: List<Carbs>,
|
||||
val effectiveProfileSwitches: List<EffectiveProfileSwitch>,
|
||||
val extendedBoluses: List<ExtendedBolus>,
|
||||
val glucoseValues: List<GlucoseValue>,
|
||||
val multiwaveBolusLinks: List<MultiwaveBolusLink>,
|
||||
val offlineEvents: List<OfflineEvent>,
|
||||
val preferencesChanges: List<PreferenceChange>,
|
||||
val profileSwitches: List<ProfileSwitch>,
|
||||
val temporaryBasals: List<TemporaryBasal>,
|
||||
val temporaryTarget: List<TemporaryTarget>,
|
||||
val therapyEvents: List<TherapyEvent>,
|
||||
val totalDailyDoses: List<TotalDailyDose>,
|
||||
val versionChanges: List<VersionChange>
|
||||
)
|
1
openhumans/.gitignore
vendored
Normal file
1
openhumans/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
15
openhumans/build.gradle
Normal file
15
openhumans/build.gradle
Normal file
|
@ -0,0 +1,15 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.hiya.jacoco-android'
|
||||
|
||||
apply from: "${project.rootDir}/gradle/android_dependencies.gradle"
|
||||
apply from: "${project.rootDir}/gradle/android_module_dependencies.gradle"
|
||||
apply from: "${project.rootDir}/gradle/test_dependencies.gradle"
|
||||
apply from: "${project.rootDir}/gradle/jacoco_global.gradle"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':database')
|
||||
}
|
0
openhumans/consumer-rules.pro
Normal file
0
openhumans/consumer-rules.pro
Normal file
21
openhumans/proguard-rules.pro
vendored
Normal file
21
openhumans/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
24
openhumans/src/main/AndroidManifest.xml
Normal file
24
openhumans/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="info.nightscout.androidaps.plugin.general.openhumans">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ui.OHLoginActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/OpenHumans"
|
||||
android:label="Setup Open Humans"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="setup-openhumans"
|
||||
android:scheme="androidaps" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,11 +1,9 @@
|
|||
package info.nightscout.androidaps.plugins.general.openhumans
|
||||
package info.nightscout.androidaps.plugin.general.openhumans
|
||||
|
||||
import java.util.*
|
||||
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
fun String.isAllowedKey() = if (startsWith("ConfigBuilder_")) true else allowedKeys.contains(this.uppercase(Locale.ROOT))
|
||||
internal fun String.isAllowedKey() = if (startsWith("ConfigBuilder_")) true else allowedKeys.contains(this.uppercase(Locale.ROOT))
|
||||
|
||||
@kotlin.ExperimentalStdlibApi
|
||||
private val allowedKeys = """
|
||||
absorption
|
||||
absorption_maxtime
|
|
@ -0,0 +1,182 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Base64
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.dagger.BaseUrl
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.dagger.ClientId
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.dagger.ClientSecret
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.dagger.RedirectUrl
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.*
|
||||
import okio.BufferedSink
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
internal class OpenHumansAPI @Inject constructor(
|
||||
@BaseUrl
|
||||
private val baseUrl: String,
|
||||
@ClientId
|
||||
clientId: String,
|
||||
@ClientSecret
|
||||
clientSecret: String,
|
||||
@RedirectUrl
|
||||
private val redirectUrl: String
|
||||
) {
|
||||
private val authHeader = "Basic " + Base64.encodeToString("$clientId:$clientSecret".toByteArray(), Base64.NO_WRAP)
|
||||
private val client = OkHttpClient()
|
||||
|
||||
suspend fun exchangeBearerToken(bearerToken: String) = sendTokenRequest(FormBody.Builder()
|
||||
.add("grant_type", "authorization_code")
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.add("code", bearerToken)
|
||||
.build())
|
||||
|
||||
suspend fun refreshAccessToken(refreshToken: String) = sendTokenRequest(FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("redirect_uri", redirectUrl)
|
||||
.add("refresh_token", refreshToken)
|
||||
.build())
|
||||
|
||||
private suspend fun sendTokenRequest(body: FormBody): OAuthTokens {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/oauth2/token/")
|
||||
.addHeader("Authorization", authHeader)
|
||||
.post(body)
|
||||
.build()
|
||||
val response = request.await()
|
||||
val json = response.body?.let { JSONObject(it.string()) }
|
||||
if (json == null || !response.isSuccessful) throw OHHttpException(response.code, response.message, json?.getString("error"))
|
||||
val accessToken = json.getString("access_token") ?: throw OHProtocolViolationException("access_token missing")
|
||||
val refreshToken = json.getString("refresh_token") ?: throw OHProtocolViolationException("refresh_token missing")
|
||||
if (!json.has("expires_in")) throw OHProtocolViolationException("expires_in missing")
|
||||
val expiresAt = timestamp + json.getInt("expires_in") * 1000L
|
||||
return OAuthTokens(accessToken, refreshToken, expiresAt)
|
||||
}
|
||||
|
||||
suspend fun getProjectMemberId(accessToken: String): String {
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/api/direct-sharing/project/exchange-member/?access_token=$accessToken")
|
||||
.get()
|
||||
.build()
|
||||
val response = request.await()
|
||||
val json = response.body?.let { JSONObject(it.string()) }
|
||||
if (json == null || !response.isSuccessful) throw OHHttpException(response.code, response.message, json?.getString("detail"))
|
||||
return json.getString("project_member_id") ?: throw OHProtocolViolationException("project_member_id missing")
|
||||
}
|
||||
|
||||
suspend fun prepareFileUpload(accessToken: String, fileName: String, metadata: FileMetadata): PreparedUpload {
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/api/direct-sharing/project/files/upload/direct/?access_token=$accessToken")
|
||||
.post(FormBody.Builder()
|
||||
.add("filename", fileName)
|
||||
.add("metadata", metadata.toJSON().toString())
|
||||
.build())
|
||||
.build()
|
||||
val response = request.await()
|
||||
val json = response.body?.let { JSONObject(it.string()) }
|
||||
if (json == null || !response.isSuccessful) throw OHHttpException(response.code, response.message, json?.getString("detail"))
|
||||
return PreparedUpload(
|
||||
fileId = json.getString("id") ?: throw OHProtocolViolationException("id missing"),
|
||||
uploadURL = json.getString("url") ?: throw OHProtocolViolationException("url missing")
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun uploadFile(url: String, content: ByteArray) {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.put(object : RequestBody() {
|
||||
override fun contentType(): MediaType? = null
|
||||
|
||||
override fun contentLength(): Long = content.size.toLong()
|
||||
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
sink.write(content)
|
||||
}
|
||||
})
|
||||
.build()
|
||||
val response = request.await()
|
||||
if (!response.isSuccessful) throw OHHttpException(response.code, response.message, null)
|
||||
}
|
||||
|
||||
suspend fun completeFileUpload(accessToken: String, fileId: String) {
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/api/direct-sharing/project/files/upload/complete/?access_token=$accessToken")
|
||||
.post(FormBody.Builder()
|
||||
.add("file_id", fileId)
|
||||
.build())
|
||||
.build()
|
||||
val response = request.await()
|
||||
if (!response.isSuccessful) throw OHHttpException(response.code, response.message, null)
|
||||
}
|
||||
|
||||
private suspend fun Request.await(): Response {
|
||||
val call = client.newCall(this)
|
||||
return suspendCancellableCoroutine {
|
||||
call.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
it.resumeWithException(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
it.resume(response, null)
|
||||
}
|
||||
})
|
||||
it.invokeOnCancellation { call.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
data class FileMetadata(
|
||||
val tags: List<String>,
|
||||
val description: String,
|
||||
val md5: String? = null,
|
||||
val creationDate: Long? = null,
|
||||
val startDate: Long? = null,
|
||||
val endDate: Long? = null
|
||||
) {
|
||||
|
||||
fun toJSON(): JSONObject {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("tags", JSONArray().apply { tags.forEach { put(it) } })
|
||||
jsonObject.put("description", description)
|
||||
jsonObject.put("md5", md5)
|
||||
creationDate?.let { jsonObject.put("creation_date", iso8601DateFormatter.format(Date(it))) }
|
||||
startDate?.let { jsonObject.put("start_date", iso8601DateFormatter.format(Date(it))) }
|
||||
endDate?.let { jsonObject.put("end_date", iso8601DateFormatter.format(Date(it))) }
|
||||
return jsonObject
|
||||
}
|
||||
}
|
||||
|
||||
data class PreparedUpload(
|
||||
val fileId: String,
|
||||
val uploadURL: String
|
||||
)
|
||||
|
||||
data class OAuthTokens(
|
||||
val accessToken: String,
|
||||
val refreshToken: String,
|
||||
val expiresAt: Long
|
||||
)
|
||||
|
||||
data class OHHttpException(
|
||||
val code: Int,
|
||||
val meaning: String,
|
||||
val detail: String?
|
||||
) : RuntimeException() {
|
||||
override val message: String get() = toString()
|
||||
}
|
||||
|
||||
class OHProtocolViolationException(
|
||||
override val message: String
|
||||
) : RuntimeException()
|
||||
|
||||
private companion object {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val iso8601DateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans
|
||||
|
||||
internal data class OpenHumansState(
|
||||
val accessToken: String,
|
||||
val refreshToken: String,
|
||||
val expiresAt: Long,
|
||||
val projectMemberId: String,
|
||||
val uploadOffset: Long
|
||||
)
|
|
@ -0,0 +1,656 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.*
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.database.AppRepository
|
||||
import info.nightscout.androidaps.database.data.Block
|
||||
import info.nightscout.androidaps.database.interfaces.TraceableDBEntry
|
||||
import info.nightscout.androidaps.events.EventPreferenceChange
|
||||
import info.nightscout.androidaps.interfaces.PluginBase
|
||||
import info.nightscout.androidaps.interfaces.PluginDescription
|
||||
import info.nightscout.androidaps.interfaces.PluginType
|
||||
import info.nightscout.androidaps.logging.AAPSLogger
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.delegates.OHAppIDDelegate
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.delegates.OHCounterDelegate
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.delegates.OHStateDelegate
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.ui.OHFragment
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.ui.OHLoginActivity
|
||||
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.rxkotlin.plusAssign
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class OpenHumansUploader @Inject internal constructor(
|
||||
injector: HasAndroidInjector,
|
||||
resourceHelper: ResourceHelper,
|
||||
aapsLogger: AAPSLogger,
|
||||
private val sp: SP,
|
||||
private val context: Context,
|
||||
private val repository: AppRepository,
|
||||
private val openHumansAPI: OpenHumansAPI,
|
||||
stateDelegate: OHStateDelegate,
|
||||
counterDelegate: OHCounterDelegate,
|
||||
appIdDelegate: OHAppIDDelegate,
|
||||
private val rxBus: RxBusWrapper
|
||||
) : PluginBase(
|
||||
PluginDescription()
|
||||
.mainType(PluginType.GENERAL)
|
||||
.pluginIcon(R.drawable.open_humans_white)
|
||||
.pluginName(R.string.open_humans)
|
||||
.shortName(R.string.open_humans_short)
|
||||
.description(R.string.open_humans_description)
|
||||
.preferencesId(R.xml.pref_openhumans)
|
||||
.fragmentClass(OHFragment::class.qualifiedName),
|
||||
aapsLogger, resourceHelper, injector
|
||||
) {
|
||||
|
||||
private var openHumansState by stateDelegate
|
||||
private var uploadCounter by counterDelegate
|
||||
private val appId by appIdDelegate
|
||||
|
||||
private val preferenceChangeDisposable = CompositeDisposable()
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setupNotificationChannels()
|
||||
if (openHumansState != null) scheduleWorker(false)
|
||||
preferenceChangeDisposable += rxBus.toObservable(EventPreferenceChange::class.java).subscribe {
|
||||
onSharedPreferenceChanged(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelWorker()
|
||||
preferenceChangeDisposable.clear()
|
||||
}
|
||||
|
||||
private fun onSharedPreferenceChanged(event: EventPreferenceChange) {
|
||||
if (event.changedKey in arrayOf("key_oh_charging_only", "key_oh_wifi_only") && openHumansState != null) scheduleWorker(true)
|
||||
}
|
||||
|
||||
suspend fun login(bearerToken: String) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val oAuthTokens = openHumansAPI.exchangeBearerToken(bearerToken)
|
||||
val projectMemberId = openHumansAPI.getProjectMemberId(oAuthTokens.accessToken)
|
||||
withContext(Dispatchers.Main) {
|
||||
openHumansState = OpenHumansState(
|
||||
accessToken = oAuthTokens.accessToken,
|
||||
refreshToken = oAuthTokens.refreshToken,
|
||||
expiresAt = oAuthTokens.expiresAt,
|
||||
projectMemberId = projectMemberId,
|
||||
uploadOffset = 0L
|
||||
)
|
||||
}
|
||||
scheduleWorker(false)
|
||||
} catch (e: Exception) {
|
||||
aapsLogger.error("Error while logging in to Open Humans", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.sha256(): String {
|
||||
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
messageDigest.update(toByteArray())
|
||||
return messageDigest.digest().toHexString()
|
||||
}
|
||||
|
||||
private fun <T : TraceableDBEntry> ZipOutputStream.writeDBEntryFile(name: String, list: List<T>, block: JSONObject.(entry: T) -> Unit) = writeJSONArrayFile(name, list) {
|
||||
put("structureVersion", 2)
|
||||
put("id", it.id)
|
||||
put("version", it.version)
|
||||
put("dateCreated", it.dateCreated)
|
||||
put("isValid", it)
|
||||
put("referenceId", it.referenceId)
|
||||
put("pumpType", it.interfaceIDs.pumpType)
|
||||
put("pumpSerialHash", it.interfaceIDs.pumpSerial?.sha256())
|
||||
put("pumpId", it.interfaceIDs.pumpId)
|
||||
put("startId", it.interfaceIDs.startId)
|
||||
put("endId", it.interfaceIDs.endId)
|
||||
block(it)
|
||||
}
|
||||
|
||||
private fun <T> ZipOutputStream.writeJSONArrayFile(name: String, list: List<T>, block: JSONObject.(entry: T) -> Unit) {
|
||||
val jsonArray = JSONArray()
|
||||
list.forEach { entry ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.block(entry)
|
||||
jsonArray.put(jsonObject)
|
||||
}
|
||||
writeFile(name, jsonArray.toString().toByteArray())
|
||||
}
|
||||
|
||||
private fun ZipOutputStream.writeFile(name: String, bytes: ByteArray) {
|
||||
putNextEntry(ZipEntry(name))
|
||||
write(bytes)
|
||||
closeEntry()
|
||||
}
|
||||
|
||||
private fun List<Block>.serialize(): JSONArray {
|
||||
val jsonArray = JSONArray()
|
||||
forEach {
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("duration", it.duration)
|
||||
jsonObject.put("amount", it.amount)
|
||||
jsonArray.put(jsonObject)
|
||||
}
|
||||
return jsonArray
|
||||
}
|
||||
|
||||
internal suspend fun uploadData() {
|
||||
try {
|
||||
withContext(Dispatchers.Default) {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val offset = openHumansState!!.uploadOffset
|
||||
var page = 0
|
||||
while (uploadDataPaged(offset, timestamp, page++));
|
||||
withContext(Dispatchers.Main) {
|
||||
openHumansState = openHumansState!!.copy(uploadOffset = timestamp)
|
||||
}
|
||||
}
|
||||
} catch (e: OpenHumansAPI.OHHttpException) {
|
||||
if (e.code == 401 && e.detail == "Invalid token.") {
|
||||
withContext(NonCancellable) {
|
||||
handleSignOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun uploadDataPaged(since: Long, until: Long, page: Int): Boolean {
|
||||
val data = repository
|
||||
.collectNewEntriesSince(since, until, 1000, 1000 * page)
|
||||
.let { data -> data.copy(preferencesChanges = data.preferencesChanges.filter { it.key.isAllowedKey() }) }
|
||||
val hasData = with(data) {
|
||||
apsResults.isNotEmpty() ||
|
||||
apsResultLinks.isNotEmpty() ||
|
||||
bolusCalculatorResults.isNotEmpty() ||
|
||||
boluses.isNotEmpty() ||
|
||||
carbs.isNotEmpty() ||
|
||||
effectiveProfileSwitches.isNotEmpty() ||
|
||||
extendedBoluses.isNotEmpty() ||
|
||||
glucoseValues.isNotEmpty() ||
|
||||
multiwaveBolusLinks.isNotEmpty() ||
|
||||
offlineEvents.isNotEmpty() ||
|
||||
preferencesChanges.isNotEmpty() ||
|
||||
profileSwitches.isNotEmpty() ||
|
||||
temporaryBasals.isNotEmpty() ||
|
||||
temporaryTarget.isNotEmpty() ||
|
||||
therapyEvents.isNotEmpty() ||
|
||||
totalDailyDoses.isNotEmpty() ||
|
||||
versionChanges.isNotEmpty()
|
||||
}
|
||||
if (!hasData) return false
|
||||
|
||||
val baos = ByteArrayOutputStream()
|
||||
val zos = ZipOutputStream(baos)
|
||||
val tags = mutableListOf<String>()
|
||||
|
||||
val applicationInfo = JSONObject()
|
||||
//TODO: Move build configuration to core module
|
||||
/*applicationInfo.put("versionName", BuildConfig.VERSION_NAME)
|
||||
applicationInfo.put("versionCode", BuildConfig.VERSION_CODE)
|
||||
val hasGitInfo = !BuildConfig.HEAD.endsWith("NoGitSystemAvailable", true)
|
||||
val customRemote = !BuildConfig.REMOTE.equals("https://github.com/nightscout/AndroidAPS.git", true)
|
||||
applicationInfo.put("hasGitInfo", hasGitInfo)
|
||||
applicationInfo.put("customRemote", customRemote)*/
|
||||
applicationInfo.put("applicationId", appId.toString())
|
||||
zos.writeFile("ApplicationInfo.json", applicationInfo.toString().toByteArray())
|
||||
tags.add("ApplicationInfo")
|
||||
|
||||
val deviceInfo = JSONObject()
|
||||
deviceInfo.put("brand", Build.BRAND)
|
||||
deviceInfo.put("device", Build.DEVICE)
|
||||
deviceInfo.put("manufacturer", Build.MANUFACTURER)
|
||||
deviceInfo.put("model", Build.MODEL)
|
||||
deviceInfo.put("product", Build.PRODUCT)
|
||||
zos.writeFile("DeviceInfo.json", deviceInfo.toString().toByteArray())
|
||||
tags.add("DeviceInfo")
|
||||
|
||||
val displayMetrics = DisplayMetrics()
|
||||
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(displayMetrics)
|
||||
val displayInfo = JSONObject()
|
||||
displayInfo.put("height", displayMetrics.heightPixels)
|
||||
displayInfo.put("width", displayMetrics.widthPixels)
|
||||
displayInfo.put("density", displayMetrics.density)
|
||||
displayInfo.put("scaledDensity", displayMetrics.scaledDensity)
|
||||
displayInfo.put("xdpi", displayMetrics.xdpi)
|
||||
displayInfo.put("ydpi", displayMetrics.ydpi)
|
||||
zos.writeFile("DisplayInfo.json", displayInfo.toString().toByteArray())
|
||||
tags.add("DisplayInfo")
|
||||
|
||||
val uploadNumber = this.uploadCounter++
|
||||
val uploadDate = Date()
|
||||
val uploadInfo = JSONObject()
|
||||
uploadInfo.put("fileVersion", 2)
|
||||
uploadInfo.put("counter", uploadNumber)
|
||||
uploadInfo.put("timestamp", until)
|
||||
uploadInfo.put("utcOffset", TimeZone.getDefault().getOffset(uploadDate.time))
|
||||
zos.writeFile("UploadInfo.json", uploadInfo.toString().toByteArray())
|
||||
tags.add("UploadInfo")
|
||||
|
||||
if (data.apsResults.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("APSResults.json", data.apsResults) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("algorithm", it.algorithm.toString())
|
||||
put("glucoseStatus", JSONObject(it.glucoseStatusJson))
|
||||
put("currentTemp", JSONObject(it.currentTempJson))
|
||||
put("iobData", JSONObject(it.iobDataJson))
|
||||
put("profile", JSONObject(it.profileJson))
|
||||
put("autosensData", JSONObject(it.autosensDataJson))
|
||||
put("mealData", JSONObject(it.mealDataJson))
|
||||
put("isMicroBolusAllowed", it.isMicroBolusAllowed)
|
||||
put("result", JSONObject(it.resultJson))
|
||||
}
|
||||
tags.add("APSResults")
|
||||
}
|
||||
|
||||
if (data.apsResultLinks.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("APSResultLinks.json", data.apsResultLinks) {
|
||||
put("apsResultId", it.apsResultId)
|
||||
put("smbId", it.smbId)
|
||||
put("tbrId", it.tbrId)
|
||||
}
|
||||
tags.add("APSResultLinks")
|
||||
}
|
||||
|
||||
if (data.bolusCalculatorResults.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("BolusCalculatorResults.json", data.bolusCalculatorResults) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("targetBGLow", it.targetBGLow)
|
||||
put("targetBGHigh", it.targetBGHigh)
|
||||
put("isf", it.isf)
|
||||
put("ic", it.ic)
|
||||
put("bolusIOB", it.bolusIOB)
|
||||
put("wasBolusIOBUsed", it.wasBolusIOBUsed)
|
||||
put("basalIOB", it.basalIOB)
|
||||
put("wasBasalIOBUsed", it.wasBasalIOBUsed)
|
||||
put("glucoseValue", it.glucoseValue)
|
||||
put("wasGlucoseUsed", it.wasGlucoseUsed)
|
||||
put("glucoseDifference", it.glucoseDifference)
|
||||
put("glucoseInsulin", it.glucoseInsulin)
|
||||
put("glucoseTrend", it.glucoseTrend)
|
||||
put("wasTrendUsed", it.wasTrendUsed)
|
||||
put("trendInsulin", it.trendInsulin)
|
||||
put("cob", it.cob)
|
||||
put("wasCOBUsed", it.wasCOBUsed)
|
||||
put("cobInsulin", it.cobInsulin)
|
||||
put("carbs", it.carbs)
|
||||
put("wereCarbsUsed", it.wereCarbsUsed)
|
||||
put("carbsInsulin", it.carbsInsulin)
|
||||
put("otherCorrection", it.otherCorrection)
|
||||
put("wasSuperbolusUsed", it.wasSuperbolusUsed)
|
||||
put("superbolusInsulin", it.superbolusInsulin)
|
||||
put("wasTempTargetUsed", it.wasTempTargetUsed)
|
||||
put("totalInsulin", it.totalInsulin)
|
||||
put("percentageCorrection", it.percentageCorrection)
|
||||
}
|
||||
tags.add("BolusCalculatorResults")
|
||||
}
|
||||
|
||||
if (data.boluses.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("Boluses.json", data.boluses) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("amount", it.amount)
|
||||
put("type", it.type.toString())
|
||||
put("isBasalInsulin", it.isBasalInsulin)
|
||||
put("insulinEndTime", it.insulinConfiguration?.insulinEndTime)
|
||||
put("peak", it.insulinConfiguration?.peak)
|
||||
}
|
||||
tags.add("Boluses")
|
||||
}
|
||||
|
||||
if (data.carbs.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("Carbs.json", data.carbs) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("duration", it.duration)
|
||||
put("amount", it.amount)
|
||||
}
|
||||
tags.add("Carbs")
|
||||
}
|
||||
|
||||
if (data.effectiveProfileSwitches.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("EffectiveProfileSwitches.json", data.effectiveProfileSwitches) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("basalBlocks", it.basalBlocks.serialize())
|
||||
put("isfBlocks", it.isfBlocks.serialize())
|
||||
put("icBlocks", it.icBlocks.serialize())
|
||||
put("icBlocks", it.icBlocks.serialize())
|
||||
val targetBlocks = JSONArray()
|
||||
it.targetBlocks.forEach { block ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("duration", block.duration)
|
||||
jsonObject.put("lowTarget", block.lowTarget)
|
||||
jsonObject.put("highTarget", block.highTarget)
|
||||
targetBlocks.put(jsonObject)
|
||||
}
|
||||
put("targetBlocks", it.targetBlocks)
|
||||
put("glucoseUnit", it.glucoseUnit.toString())
|
||||
put("originalTimeshift", it.originalTimeshift)
|
||||
put("originalPercentage", it.originalPercentage)
|
||||
put("originalDuration", it.originalDuration)
|
||||
put("originalEnd", it.originalEnd)
|
||||
put("insulinEndTime", it.insulinConfiguration.insulinEndTime)
|
||||
put("insulinEndTime", it.insulinConfiguration.peak)
|
||||
}
|
||||
tags.add("EffectiveProfileSwitches")
|
||||
}
|
||||
|
||||
if (data.extendedBoluses.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("ExtendedBoluses.json", data.extendedBoluses) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("duration", it.duration)
|
||||
put("amount", it.amount)
|
||||
put("isEmulatingTempBasal", it.isEmulatingTempBasal)
|
||||
}
|
||||
tags.add("ExtendedBoluses")
|
||||
}
|
||||
|
||||
if (data.glucoseValues.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("GlucoseValues.json", data.glucoseValues) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("raw", it.raw)
|
||||
put("value", it.value)
|
||||
put("trendArrow", it.trendArrow.toString())
|
||||
put("noise", it.noise)
|
||||
put("sourceSensor", it.sourceSensor.toString())
|
||||
}
|
||||
tags.add("GlucoseValues")
|
||||
}
|
||||
|
||||
if (data.multiwaveBolusLinks.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("MultiwaveBolusLinks.json", data.multiwaveBolusLinks) {
|
||||
put("bolusId", it.bolusId)
|
||||
put("extendedBolusId", it.extendedBolusId)
|
||||
}
|
||||
tags.add("MultiwaveBolusLinks")
|
||||
}
|
||||
|
||||
if (data.offlineEvents.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("OfflineEvents.json", data.offlineEvents) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("reason", it.reason.toString())
|
||||
put("duration", it.duration)
|
||||
}
|
||||
tags.add("OfflineEvents")
|
||||
}
|
||||
|
||||
if (data.preferencesChanges.isNotEmpty()) {
|
||||
zos.writeJSONArrayFile("PreferenceChanges.json", data.preferencesChanges) {
|
||||
put("structureVersion", 2)
|
||||
put("id", it.id)
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("structureVersion", 2)
|
||||
put("key", it.key)
|
||||
put("value", it.value)
|
||||
}
|
||||
tags.add("PreferenceChanges")
|
||||
}
|
||||
|
||||
if (data.profileSwitches.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("ProfileSwitches.json", data.profileSwitches) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("basalBlocks", it.basalBlocks.serialize())
|
||||
put("isfBlocks", it.basalBlocks.serialize())
|
||||
put("icBlocks", it.icBlocks.serialize())
|
||||
put("basalBlocks", it.basalBlocks.serialize())
|
||||
val targetBlocks = JSONArray()
|
||||
it.targetBlocks.forEach { block ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("duration", block.duration)
|
||||
jsonObject.put("lowTarget", block.lowTarget)
|
||||
jsonObject.put("highTarget", block.highTarget)
|
||||
targetBlocks.put(jsonObject)
|
||||
}
|
||||
put("glucoseUnit", it.glucoseUnit.toString())
|
||||
put("timeshift", it.timeshift)
|
||||
put("percentage", it.percentage)
|
||||
put("duration", it.duration)
|
||||
put("insulinEndTime", it.insulinConfiguration.insulinEndTime)
|
||||
put("peak", it.insulinConfiguration.peak)
|
||||
}
|
||||
tags.add("ProfileSwitches")
|
||||
}
|
||||
|
||||
if (data.temporaryBasals.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("TemporaryBasals.json", data.temporaryBasals) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("type", it.type.toString())
|
||||
put("isAbsolute", it.isAbsolute)
|
||||
put("rate", it.rate)
|
||||
put("duration", it.duration)
|
||||
}
|
||||
tags.add("TemporaryBasals")
|
||||
}
|
||||
|
||||
if (data.temporaryTarget.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("TemporaryTargets.json", data.temporaryTarget) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("reason", it.reason.toString())
|
||||
put("highTarget", it.highTarget)
|
||||
put("lowTarget", it.lowTarget)
|
||||
put("duration", it.duration)
|
||||
}
|
||||
tags.add("TemporaryTargets")
|
||||
}
|
||||
|
||||
if (data.therapyEvents.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("TherapyEvents.json", data.therapyEvents) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("type", it.type.toString())
|
||||
put("glucose", it.glucose)
|
||||
put("glucoseType", it.glucoseType?.toString())
|
||||
put("glucoseUnit", it.glucoseUnit.toString())
|
||||
}
|
||||
tags.add("TherapyEvents")
|
||||
}
|
||||
|
||||
if (data.totalDailyDoses.isNotEmpty()) {
|
||||
zos.writeDBEntryFile("TotalDailyDoses.json", data.totalDailyDoses) {
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("basalAmount", it.basalAmount)
|
||||
put("bolusAmount", it.bolusAmount)
|
||||
put("totalAmount", it.totalAmount)
|
||||
put("carbs", it.carbs)
|
||||
}
|
||||
tags.add("TotalDailyDoses")
|
||||
}
|
||||
|
||||
if (data.versionChanges.isNotEmpty()) {
|
||||
zos.writeJSONArrayFile("VersionChanges.json", data.versionChanges) {
|
||||
put("structureVersion", 2)
|
||||
put("id", it.id)
|
||||
put("timestamp", it.timestamp)
|
||||
put("utcOffset", it.utcOffset)
|
||||
put("versionCode", it.versionCode)
|
||||
put("versionName", it.versionName)
|
||||
val customGitRemote = it.gitRemote != "https://github.com/nightscout/AndroidAPS.git"
|
||||
put("customGitRemote", customGitRemote)
|
||||
put("commitHash", if (customGitRemote) null else it.commitHash)
|
||||
}
|
||||
tags.add("VersionChanges")
|
||||
}
|
||||
|
||||
zos.close()
|
||||
val bytes = baos.toByteArray()
|
||||
|
||||
val fileName = "upload-num$uploadNumber-ver2-date${FILE_NAME_DATE_FORMAT.format(uploadDate)}-appid${appId.toString().replace("-", "")}.zip"
|
||||
|
||||
val metaData = OpenHumansAPI.FileMetadata(
|
||||
tags = tags,
|
||||
description = "AndroidAPS Database Upload",
|
||||
md5 = MessageDigest.getInstance("MD5").digest(bytes).toHexString(),
|
||||
creationDate = uploadDate.time
|
||||
)
|
||||
|
||||
refreshAccessTokenIfNeeded()
|
||||
|
||||
val preparedUpload = openHumansAPI.prepareFileUpload(openHumansState!!.accessToken, fileName, metaData)
|
||||
openHumansAPI.uploadFile(preparedUpload.uploadURL, bytes)
|
||||
openHumansAPI.completeFileUpload(openHumansState!!.accessToken, preparedUpload.fileId)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun cancelWorker() = WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME_PERIODIC)
|
||||
|
||||
private fun scheduleWorker(replace: Boolean, delay: Boolean = false) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(if (sp.getBoolean("key_oh_wifi_only", true)) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||
.setRequiresCharging(sp.getBoolean("key_oh_charging_only", false))
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
val workRequest = PeriodicWorkRequestBuilder<OpenHumansWorker>(1, TimeUnit.DAYS)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 20, TimeUnit.MINUTES)
|
||||
.setInitialDelay(if (delay) 1 else 0, TimeUnit.DAYS)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_NAME_PERIODIC, if (replace) ExistingPeriodicWorkPolicy.REPLACE else ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
||||
}
|
||||
|
||||
internal fun uploadNow() {
|
||||
val workRequest = OneTimeWorkRequestBuilder<OpenHumansWorker>()
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, workRequest)
|
||||
scheduleWorker(replace = true, delay = true)
|
||||
}
|
||||
|
||||
private fun ByteArray.toHexString(): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
map { it.toInt() }.forEach {
|
||||
stringBuilder.append(HEX_DIGITS[(it shr 4) and 0x0F])
|
||||
stringBuilder.append(HEX_DIGITS[it and 0x0F])
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
private suspend fun refreshAccessTokenIfNeeded() {
|
||||
val state = openHumansState!!
|
||||
if (state.expiresAt <= System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)) {
|
||||
val newTokens = openHumansAPI.refreshAccessToken(state.refreshToken)
|
||||
withContext(Dispatchers.Main) {
|
||||
openHumansState = state.copy(
|
||||
accessToken = newTokens.accessToken,
|
||||
refreshToken = newTokens.refreshToken,
|
||||
expiresAt = newTokens.expiresAt
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
cancelWorker()
|
||||
openHumansState = null
|
||||
}
|
||||
|
||||
private fun setupNotificationChannels() {
|
||||
val notificationManagerCompat = NotificationManagerCompat.from(context)
|
||||
notificationManagerCompat.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_WORKER,
|
||||
resourceHelper.gs(R.string.open_humans_uploading),
|
||||
NotificationManager.IMPORTANCE_MIN
|
||||
)
|
||||
)
|
||||
notificationManagerCompat.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_MESSAGES,
|
||||
resourceHelper.gs(R.string.open_humans_notifications),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleSignOut() {
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_MESSAGES)
|
||||
.setContentTitle(resourceHelper.gs(R.string.you_have_been_signed_out_of_open_humans))
|
||||
.setContentText(resourceHelper.gs(R.string.click_here_to_sign_in_again_if_this_wasnt_on_purpose))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setSmallIcon(R.drawable.open_humans_notification)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
Intent(context, OHLoginActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
},
|
||||
0
|
||||
))
|
||||
.build()
|
||||
NotificationManagerCompat.from(context).notify(SIGNED_OUT_NOTIFICATION_ID, notification)
|
||||
withContext(Dispatchers.Main) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createForegroundInfo(id: UUID): ForegroundInfo {
|
||||
val cancel = context.getString(R.string.cancel)
|
||||
|
||||
val intent = WorkManager.getInstance(context)
|
||||
.createCancelPendingIntent(id)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_WORKER)
|
||||
.setContentTitle(context.getString(R.string.open_humans_uploading))
|
||||
.setContentText(context.getString(R.string.uploading_to_open_humans))
|
||||
.setSmallIcon(R.drawable.open_humans_notification)
|
||||
.setOngoing(true)
|
||||
.addAction(android.R.drawable.ic_delete, cancel, intent)
|
||||
.build()
|
||||
|
||||
return ForegroundInfo(UPLOAD_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val HEX_DIGITS = "0123456789ABCDEF".toCharArray()
|
||||
@Suppress("PrivatePropertyName")
|
||||
private val FILE_NAME_DATE_FORMAT = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
const val WORK_NAME_PERIODIC = "Open Humans Periodic"
|
||||
const val WORK_NAME_MANUAL = "Open Humans Manual"
|
||||
const val NOTIFICATION_CHANNEL_WORKER = "OpenHumansWorker"
|
||||
const val NOTIFICATION_CHANNEL_MESSAGES = "OpenHumansMessages"
|
||||
const val SIGNED_OUT_NOTIFICATION_ID = 3125
|
||||
const val UPLOAD_NOTIFICATION_ID = 3126
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.android.HasAndroidInjector
|
||||
import info.nightscout.androidaps.logging.AAPSLogger
|
||||
import info.nightscout.androidaps.logging.LTag
|
||||
import javax.inject.Inject
|
||||
|
||||
class OpenHumansWorker(
|
||||
context: Context,
|
||||
workerParameters: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParameters) {
|
||||
|
||||
init {
|
||||
(applicationContext as HasAndroidInjector).androidInjector().inject(this)
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var logger: AAPSLogger
|
||||
|
||||
@Inject
|
||||
lateinit var openHumansUploader: OpenHumansUploader
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
logger.info(LTag.OHUPLOADER, "Starting upload")
|
||||
setForeground(openHumansUploader.createForegroundInfo(id))
|
||||
openHumansUploader.uploadData()
|
||||
logger.info(LTag.OHUPLOADER, "Upload finished")
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
logger.error(LTag.OHUPLOADER, "OH Uploader failed", e)
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.dagger
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.MapKey
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Qualifier
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class BaseUrl
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class ClientId
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class ClientSecret
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class RedirectUrl
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class AuthUrl
|
||||
|
||||
@MapKey
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class ViewModelFactory @Inject constructor(
|
||||
private val viewModels: MutableMap<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.dagger
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import dagger.multibindings.IntoMap
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansWorker
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.delegates.OHStateDelegate
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.ui.OHFragment
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.ui.OHLoginActivity
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.ui.OHLoginViewModel
|
||||
|
||||
@Module
|
||||
abstract class OpenHumansModule {
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesOHLoginActivity(): OHLoginActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesOHFragment(): OHFragment
|
||||
|
||||
@ContributesAndroidInjector abstract fun contributesOpenHumansWorker(): OpenHumansWorker
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(OHLoginViewModel::class)
|
||||
internal abstract fun bindLoginViewModel(viewModel: OHLoginViewModel): ViewModel
|
||||
|
||||
companion object {
|
||||
|
||||
@BaseUrl
|
||||
@Provides
|
||||
fun providesBaseUrl(): String = "https://www.openhumans.org"
|
||||
|
||||
@ClientId
|
||||
@Provides
|
||||
fun providesClientId(): String = "oie6DvnaEOagTxSoD6BukkLPwDhVr6cMlN74Ihz1"
|
||||
|
||||
@ClientSecret
|
||||
@Provides
|
||||
fun providesClientSecret(): String =
|
||||
"jR0N8pkH1jOwtozHc7CsB1UPcJzFN95ldHcK4VGYIApecr8zGJox0v06xLwPLMASScngT12aIaIHXAVCJeKquEXAWG1XekZdbubSpccgNiQBmuVmIF8nc1xSKSNJltCf"
|
||||
|
||||
@RedirectUrl
|
||||
@Provides
|
||||
fun providesRedirectUri(): String = "androidaps://setup-openhumans"
|
||||
|
||||
@AuthUrl
|
||||
@Provides
|
||||
internal fun providesAuthUrl(@ClientId clientId: String): String =
|
||||
"https://www.openhumans.org/direct-sharing/projects/oauth2/authorize/?client_id=$clientId&response_type=code"
|
||||
|
||||
@Provides
|
||||
internal fun providesStateLiveData(ohStateDelegate: OHStateDelegate) = ohStateDelegate.value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.delegates
|
||||
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@Singleton
|
||||
internal class OHAppIDDelegate @Inject internal constructor(
|
||||
private val sp: SP
|
||||
) {
|
||||
private var value: UUID? = null
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): UUID {
|
||||
if (value == null) {
|
||||
val saved = sp.getStringOrNull("openhumans_appid", null)
|
||||
if (saved == null) {
|
||||
val generated = UUID.randomUUID()
|
||||
value = generated
|
||||
sp.putString("openhumans_appid", generated.toString())
|
||||
} else {
|
||||
value = UUID.fromString(saved)
|
||||
}
|
||||
}
|
||||
return value!!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.delegates
|
||||
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@Singleton
|
||||
internal class OHCounterDelegate @Inject internal constructor(
|
||||
private val sp: SP
|
||||
) {
|
||||
private var value = sp.getLong("openhumans_counter", 1)
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): Long = value
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) {
|
||||
this.value = value
|
||||
sp.putLong("openhumans_counter", value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.delegates
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansState
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@Singleton
|
||||
internal class OHStateDelegate @Inject internal constructor(
|
||||
private val sp: SP
|
||||
) {
|
||||
|
||||
private var _value = MutableLiveData(loadState())
|
||||
val value = _value as LiveData<OpenHumansState?>
|
||||
|
||||
private fun loadState(): OpenHumansState? {
|
||||
return OpenHumansState(
|
||||
accessToken = sp.getStringOrNull("openhumans_access_token", null) ?: return null,
|
||||
refreshToken = sp.getStringOrNull("openhumans_refresh_token", null) ?: return null,
|
||||
expiresAt = if (sp.contains("openhumans_expires_at"))
|
||||
sp.getLong("openhumans_expires_at", 0)
|
||||
else
|
||||
return null,
|
||||
projectMemberId = sp.getStringOrNull("openhumans_project_member_id", null)
|
||||
?: return null,
|
||||
uploadOffset = sp.getLong("openhumans_upload_offset", 0)
|
||||
)
|
||||
}
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): OpenHumansState? = _value.value
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: OpenHumansState?) {
|
||||
this._value.value = value
|
||||
if (value == null) {
|
||||
sp.remove("openhumans_access_token")
|
||||
sp.remove("openhumans_refresh_token")
|
||||
sp.remove("openhumans_expires_at")
|
||||
sp.remove("openhumans_project_member_id")
|
||||
sp.remove("openhumans_upload_offset")
|
||||
} else {
|
||||
sp.putString("openhumans_access_token", value.accessToken)
|
||||
sp.putString("openhumans_refresh_token", value.refreshToken)
|
||||
sp.putLong("openhumans_expires_at", value.expiresAt)
|
||||
sp.putString("openhumans_project_member_id", value.projectMemberId)
|
||||
sp.putLong("openhumans_upload_offset", value.uploadOffset)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import dagger.android.support.DaggerFragment
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.R
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansState
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansUploader
|
||||
import javax.inject.Inject
|
||||
|
||||
class OHFragment : DaggerFragment() {
|
||||
|
||||
@Inject
|
||||
internal lateinit var stateLiveData: LiveData<OpenHumansState?>
|
||||
@Inject
|
||||
internal lateinit var plugin: OpenHumansUploader
|
||||
|
||||
private lateinit var setup: MaterialButton
|
||||
private lateinit var logout: MaterialButton
|
||||
private lateinit var uploadNow: MaterialButton
|
||||
private lateinit var info: TextView
|
||||
private lateinit var memberId: TextView
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val contextWrapper = ContextThemeWrapper(requireActivity(), R.style.OpenHumans)
|
||||
val wrappedInflater = inflater.cloneInContext(contextWrapper)
|
||||
val view = wrappedInflater.inflate(R.layout.fragment_open_humans_new, container, false)
|
||||
setup = view.findViewById(R.id.setup)
|
||||
setup.setOnClickListener { startActivity(Intent(context, OHLoginActivity::class.java)) }
|
||||
logout = view.findViewById(R.id.logout)
|
||||
logout.setOnClickListener { plugin.logout() }
|
||||
info = view.findViewById(R.id.info)
|
||||
memberId = view.findViewById(R.id.member_id)
|
||||
uploadNow = view.findViewById(R.id.upload_now)
|
||||
uploadNow.setOnClickListener { plugin.uploadNow() }
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
stateLiveData.observe(this) { state ->
|
||||
if (state == null) {
|
||||
setup.visibility = View.VISIBLE
|
||||
logout.visibility = View.GONE
|
||||
memberId.visibility = View.GONE
|
||||
uploadNow.visibility = View.GONE
|
||||
info.setText(R.string.not_setup_info)
|
||||
} else {
|
||||
setup.visibility = View.GONE
|
||||
logout.visibility = View.VISIBLE
|
||||
memberId.visibility = View.VISIBLE
|
||||
uploadNow.visibility = View.VISIBLE
|
||||
memberId.text = getString(R.string.project_member_id, state.projectMemberId)
|
||||
info.setText(R.string.setup_completed_info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import androidx.activity.viewModels
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import dagger.android.support.DaggerAppCompatActivity
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.R
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.dagger.AuthUrl
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.dagger.ViewModelFactory
|
||||
import javax.inject.Inject
|
||||
|
||||
class OHLoginActivity : DaggerAppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
internal lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
@Inject
|
||||
@AuthUrl
|
||||
internal lateinit var authUrl: String
|
||||
|
||||
private val viewModel by viewModels<OHLoginViewModel> { viewModelFactory }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_open_humans_login_new)
|
||||
|
||||
val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar!!.setDisplayShowHomeEnabled(true)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(toolbar) { view, insets ->
|
||||
view.updatePadding(top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top)
|
||||
insets
|
||||
}
|
||||
|
||||
val accept = findViewById<CheckBox>(R.id.accept)
|
||||
val login = findViewById<MaterialButton>(R.id.login)
|
||||
accept.setOnCheckedChangeListener { _, value -> login.isEnabled = value }
|
||||
|
||||
login.setOnClickListener {
|
||||
CustomTabsIntent.Builder().build().launchUrl(this, Uri.parse(authUrl))
|
||||
}
|
||||
|
||||
val cancel = findViewById<MaterialButton>(R.id.cancel)
|
||||
cancel.setOnClickListener { viewModel.cancel() }
|
||||
|
||||
val proceed = findViewById<MaterialButton>(R.id.proceed)
|
||||
proceed.setOnClickListener { viewModel.finish() }
|
||||
|
||||
val close = findViewById<MaterialButton>(R.id.close)
|
||||
close.setOnClickListener { finish() }
|
||||
|
||||
val welcome = findViewById<NestedScrollView>(R.id.welcome)
|
||||
val consent = findViewById<NestedScrollView>(R.id.consent)
|
||||
val confirm = findViewById<NestedScrollView>(R.id.confirm)
|
||||
val finishing = findViewById<NestedScrollView>(R.id.finishing)
|
||||
val done = findViewById<NestedScrollView>(R.id.done)
|
||||
|
||||
val welcomeNext = findViewById<MaterialButton>(R.id.welcome_next)
|
||||
welcomeNext.setOnClickListener {
|
||||
viewModel.goToConsent()
|
||||
}
|
||||
|
||||
viewModel.state.observe(this) { state ->
|
||||
welcome.visibility =
|
||||
if (state == OHLoginViewModel.State.WELCOME) View.VISIBLE else View.GONE
|
||||
consent.visibility =
|
||||
if (state == OHLoginViewModel.State.CONSENT) View.VISIBLE else View.GONE
|
||||
confirm.visibility =
|
||||
if (state == OHLoginViewModel.State.CONFIRM) View.VISIBLE else View.GONE
|
||||
finishing.visibility =
|
||||
if (state == OHLoginViewModel.State.FINISHING) View.VISIBLE else View.GONE
|
||||
done.visibility =
|
||||
if (state == OHLoginViewModel.State.DONE) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
viewModel.submitBearerToken(code)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
viewModel.submitBearerToken(code)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!viewModel.goBack()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean =
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package info.nightscout.androidaps.plugin.general.openhumans.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import info.nightscout.androidaps.plugin.general.openhumans.OpenHumansUploader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class OHLoginViewModel @Inject constructor(
|
||||
private val plugin: OpenHumansUploader
|
||||
) : ViewModel(), CoroutineScope by MainScope() {
|
||||
|
||||
private val _state = MutableLiveData(State.WELCOME)
|
||||
val state = _state as LiveData<State>
|
||||
|
||||
private var bearerToken = ""
|
||||
|
||||
fun goToConsent() {
|
||||
_state.value = State.CONSENT
|
||||
}
|
||||
|
||||
fun goBack() = when (_state.value) {
|
||||
State.CONSENT -> {
|
||||
_state.value = State.WELCOME
|
||||
true
|
||||
}
|
||||
State.CONFIRM -> {
|
||||
_state.value = State.CONSENT
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun submitBearerToken(bearerToken: String) {
|
||||
if (_state.value == State.WELCOME || _state.value == State.CONSENT) {
|
||||
this.bearerToken = bearerToken
|
||||
_state.value = State.CONFIRM
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
_state.value = State.CONSENT
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
_state.value = State.FINISHING
|
||||
launch {
|
||||
try {
|
||||
plugin.login(bearerToken)
|
||||
_state.value = State.DONE
|
||||
} catch (e: Exception) {
|
||||
_state.value = State.CONSENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
enum class State {
|
||||
WELCOME,
|
||||
CONSENT,
|
||||
CONFIRM,
|
||||
FINISHING,
|
||||
DONE
|
||||
}
|
||||
}
|
6
openhumans/src/main/res/drawable/dot.xml
Normal file
6
openhumans/src/main/res/drawable/dot.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid
|
||||
android:color="?colorOnBackground"/>
|
||||
</shape>
|
10
openhumans/src/main/res/drawable/open_humans.xml
Normal file
10
openhumans/src/main/res/drawable/open_humans.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector android:height="100.5272dp" android:viewportHeight="108.36626"
|
||||
android:viewportWidth="107.79796" android:width="100dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?colorPrimary" android:pathData="m59.75,62.316c5.179,-2.256 8.801,-7.417 8.801,-13.427 0,-8.086 -6.555,-14.641 -14.641,-14.641 -8.086,0 -14.641,6.555 -14.641,14.641 0,6.01 3.622,11.171 8.801,13.427 -7.849,1.589 -14.555,6.318 -18.76,12.817 5.968,6.896 14.774,11.272 24.589,11.272 9.821,0 18.633,-4.382 24.601,-11.286 -4.205,-6.491 -10.907,-11.215 -18.75,-12.803z"/>
|
||||
<path android:fillColor="?colorPrimary" android:pathData="M21.689,33.33 L10.002,21.643c-5.155,7 -8.677,15.271 -10.002,24.25l16.523,0c0.968,-4.535 2.741,-8.776 5.166,-12.563z"/>
|
||||
<path android:fillColor="?colorPrimary" android:pathData="m91.275,45.893l16.523,0C106.473,36.909 102.947,28.634 97.787,21.631L86.101,33.317c2.429,3.79 4.205,8.035 5.174,12.576z"/>
|
||||
<path android:fillColor="?colorPrimary" android:pathData="M86.305,10.106C79.304,4.91 71.02,1.351 62.022,0l0,15.422l13.059,5.908z"/>
|
||||
<path android:fillColor="?colorPrimary" android:pathData="M45.754,15.339L45.754,0.003c-8.995,1.354 -17.276,4.915 -24.274,10.113l10.963,10.963z"/>
|
||||
<path android:fillColor="?colorSecondary" android:pathData="m26.558,80.554c-4.881,-5.002 -8.405,-11.333 -9.971,-18.394l-16.546,0c4.001,26.128 26.629,46.206 53.858,46.206 27.229,0 49.857,-20.077 53.858,-46.206l-16.546,0c-1.564,7.053 -5.082,13.378 -9.955,18.378 -6.946,7.127 -16.643,11.56 -27.357,11.56 -10.706,0 -20.396,-4.427 -27.341,-11.544z"/>
|
||||
</vector>
|
|
@ -0,0 +1,24 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="410dp"
|
||||
android:height="410dp"
|
||||
android:viewportWidth="410"
|
||||
android:viewportHeight="410">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M227.1,235.731c19.569,-8.537 33.248,-28.056 33.248,-50.77c0,-30.552 -24.761,-55.313 -55.313,-55.313c-30.552,0 -55.313,24.761 -55.313,55.313c0,22.714 13.679,42.234 33.248,50.77c-29.654,5.991 -55.014,23.863 -70.889,48.424c22.565,26.059 55.862,42.583 92.954,42.583c37.142,0 70.44,-16.574 93.004,-42.633C282.064,259.593 256.754,241.721 227.1,235.731L227.1,235.731z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M83.276,126.202l-44.181,-44.181c-19.469,26.459 -32.799,57.71 -37.791,91.656l62.452,0C67.401,156.505 74.09,140.48 83.276,126.202L83.276,126.202z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M346.264,173.678l62.452,0c-4.992,-33.947 -18.321,-65.248 -37.841,-91.706l-44.181,44.181C335.88,140.48 342.619,156.505 346.264,173.678z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M327.493,38.39C301.035,18.771 269.734,5.342 235.687,0.2l0,58.309l49.373,22.315L327.493,38.39z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M174.233,58.209L174.233,0.25C140.236,5.342 108.936,18.821 82.477,38.44l41.435,41.435L174.233,58.209z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M101.647,304.673c-18.471,-18.92 -31.75,-42.833 -37.691,-69.541L1.454,235.132c15.126,98.745 100.642,174.626 203.581,174.626s188.455,-75.881 203.581,-174.626L346.014,235.132c-5.891,26.658 -19.22,50.571 -37.641,69.441c-26.259,26.958 -62.901,43.682 -103.388,43.682C164.548,348.304 127.906,331.581 101.647,304.673z"/>
|
||||
</vector>
|
12
openhumans/src/main/res/drawable/tick_mark.xml
Normal file
12
openhumans/src/main/res/drawable/tick_mark.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="160dp"
|
||||
android:height="121.82dp"
|
||||
android:viewportWidth="160"
|
||||
android:viewportHeight="121.82">
|
||||
<path
|
||||
android:pathData="M12.73,58.18 L50.91,96.36v25.46L0,70.91Z"
|
||||
android:fillColor="?colorSecondary"/>
|
||||
<path
|
||||
android:pathData="M160,12.73 L50.91,121.82V96.36L147.27,0Z"
|
||||
android:fillColor="?colorPrimary"/>
|
||||
</vector>
|
19
openhumans/src/main/res/font/montserrat.xml
Normal file
19
openhumans/src/main/res/font/montserrat.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<font
|
||||
android:fontStyle="normal"
|
||||
android:fontWeight="300"
|
||||
android:font="@font/montserrat_light" />
|
||||
<font
|
||||
android:fontStyle="normal"
|
||||
android:fontWeight="400"
|
||||
android:font="@font/montserrat_regular" />
|
||||
<font
|
||||
android:fontStyle="normal"
|
||||
android:fontWeight="500"
|
||||
android:font="@font/montserrat_medium" />
|
||||
<font
|
||||
android:fontStyle="normal"
|
||||
android:fontWeight="700"
|
||||
android:font="@font/montserrat_bold" />
|
||||
</font-family>
|
BIN
openhumans/src/main/res/font/montserrat_bold.ttf
Normal file
BIN
openhumans/src/main/res/font/montserrat_bold.ttf
Normal file
Binary file not shown.
BIN
openhumans/src/main/res/font/montserrat_light.ttf
Normal file
BIN
openhumans/src/main/res/font/montserrat_light.ttf
Normal file
Binary file not shown.
BIN
openhumans/src/main/res/font/montserrat_medium.ttf
Normal file
BIN
openhumans/src/main/res/font/montserrat_medium.ttf
Normal file
Binary file not shown.
BIN
openhumans/src/main/res/font/montserrat_regular.ttf
Normal file
BIN
openhumans/src/main/res/font/montserrat_regular.ttf
Normal file
Binary file not shown.
|
@ -0,0 +1,723 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
tools:context="info.nightscout.androidaps.plugin.general.openhumans.ui.OHLoginActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:animateLayoutChanges="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/welcome"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground"
|
||||
android:fillViewport="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:srcCompat="@drawable/open_humans" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="Welcome to Open Humans"
|
||||
android:textAppearance="?textAppearanceHeadline5"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/open_humans_description"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="To setup data upload, click 'Next'."
|
||||
android:textAppearance="?textAppearanceSubtitle1"
|
||||
android:textColor="?colorSecondary" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/welcome_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Next" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/consent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground"
|
||||
android:fillViewport="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Consent"
|
||||
android:textAppearance="?textAppearanceHeadline5"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Please carefully read the following information and accept the terms of use to proceed."
|
||||
android:textAppearance="?textAppearanceSubtitle1"
|
||||
android:textColor="?colorSecondary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="Terms of Use"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="This is an open source tool that will copy your data to Open Humans. We retain no rights to share your data with third parties without your explicit authorization. The data the project and app receive are identified via a random user ID and will only be securely transmitted to an Open Humans account with your authorization of that process. You can stop uploading and delete your upload data at any time via www.openhumans.org."
|
||||
android:textAppearance="?textAppearanceBody2"
|
||||
android:textColor="?colorOnBackground" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="Data Uploaded"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Glucose values"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Boluses"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Extended boluses"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Carbohydrates"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Careportal Events (except notes)"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Profile Switches"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Total Daily Doses"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Temporary Basal Rates"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Temporary Targets"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Settings"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Application Version"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Device Model"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Screen Dimensions"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Algorithm Debug Data"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="Data NOT Uploaded"
|
||||
android:textAppearance="?textAppearanceHeadline6"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Passwords"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Nightscout URL"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Nightscout API Secret"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<View
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/dot" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Free Text Fields"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
android:textColor="?colorOnBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/accept"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="I understand and agree." />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:enabled="false"
|
||||
android:text="Login to Open Humans" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground"
|
||||
android:fillViewport="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="Final touches"
|
||||
android:textAppearance="?textAppearanceHeadline4"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="You are just one step away from uploading your data to Open Humans. Do you want to proceed?"
|
||||
android:textAppearance="?textAppearanceBody1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Cancel" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/proceed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Proceed" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/finishing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground"
|
||||
android:fillViewport="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="Finishing..."
|
||||
android:textAppearance="?textAppearanceHeadline4"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="This may take a few seconds."
|
||||
android:textAppearance="?textAppearanceBody1" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="true" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/done"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground"
|
||||
android:fillViewport="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/tick_mark" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:text="We're done!"
|
||||
android:textAppearance="?textAppearanceHeadline4"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="From now on, your phone will silently upload data in the background from time to time."
|
||||
android:textAppearance="?textAppearanceBody1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Close" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorSurface"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4,6 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
|
@ -11,7 +12,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="200dp"
|
||||
|
@ -20,44 +22,51 @@
|
|||
android:paddingBottom="16dp"
|
||||
app:srcCompat="@drawable/open_humans" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/open_humans"
|
||||
android:textAppearance="?textAppearanceHeadline5"
|
||||
android:textColor="?colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/setup_completed_info"
|
||||
android:textAppearance="?textAppearanceBody2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/member_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
tools:text="Project Member ID: 5151515" />
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="8dp"
|
||||
android:id="@+id/queue_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="Queue Size: 155" />
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="8dp"
|
||||
android:id="@+id/worker_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="Worker State: Running" />
|
||||
|
||||
<Button
|
||||
android:layout_marginTop="16dp"
|
||||
android:id="@+id/login"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/setup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login" />
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/setup" />
|
||||
|
||||
<Button
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/upload_now"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/upload_now" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/logout"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/logout" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
7
openhumans/src/main/res/values/colors.xml
Normal file
7
openhumans/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="open_humans_orange">#ff9161</color>
|
||||
<color name="open_humans_orange_variant">#fe6315</color>
|
||||
<color name="open_humans_blue">#009fa8</color>
|
||||
<color name="open_humans_blue_variant">#036866</color>
|
||||
</resources>
|
20
openhumans/src/main/res/values/strings.xml
Normal file
20
openhumans/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="open_humans">Open Humans</string>
|
||||
<string name="open_humans_short">OH</string>
|
||||
<string name="open_humans_description">Open Humans allows you to upload your diabetes data and donate it to scientific projects.</string>
|
||||
<string name="logout">Logout</string>
|
||||
<string name="setup">Setup</string>
|
||||
<string name="setup_completed_info">Open Humans has been setup. In case you want to stop uploading, click \'Logout\'.</string>
|
||||
<string name="not_setup_info">Open Humans is currently inactive. To start uploading data, click \'Setup\'.</string>
|
||||
<string name="project_member_id">Project Member ID: %1$s</string>
|
||||
<string name="only_upload_if_connected_to_wifi">Only upload if connected to WiFi</string>
|
||||
<string name="only_upload_if_charging">Only upload if charging</string>
|
||||
<string name="key_open_humans_settings" translatable="false">open_humans</string>
|
||||
<string name="open_humans_uploading">Uploading to Open Humans…</string>
|
||||
<string name="open_humans_notifications">Open Humans Notifications</string>
|
||||
<string name="uploading_to_open_humans">AndroidAPS is uploading to Open Humans. This may take a while.</string>
|
||||
<string name="you_have_been_signed_out_of_open_humans">You have been signed out of Open Humans</string>
|
||||
<string name="click_here_to_sign_in_again_if_this_wasnt_on_purpose">Click here to sign in again if this wasn\'t on purpose.</string>
|
||||
<string name="upload_now">Upload now</string>
|
||||
</resources>
|
13
openhumans/src/main/res/values/styles.xml
Normal file
13
openhumans/src/main/res/values/styles.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="OpenHumans" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/open_humans_orange</item>
|
||||
<item name="colorSecondary">@color/open_humans_blue</item>
|
||||
<item name="colorPrimaryVariant">@color/open_humans_orange_variant</item>
|
||||
<item name="colorSecondaryVariant">@color/open_humans_blue_variant</item>
|
||||
<item name="colorOnPrimary">@android:color/white</item>
|
||||
<item name="colorOnSecondary">@android:color/white</item>
|
||||
<item name="fontFamily">@font/montserrat</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
</resources>
|
22
openhumans/src/main/res/xml/pref_openhumans.xml
Normal file
22
openhumans/src/main/res/xml/pref_openhumans.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:validate="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/key_open_humans_settings"
|
||||
android:title="@string/open_humans">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="true"
|
||||
android:key="key_oh_wifi_only"
|
||||
android:title="@string/only_upload_if_connected_to_wifi" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="key_oh_charging_only"
|
||||
android:title="@string/only_upload_if_charging" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
|
@ -16,3 +16,4 @@ include ':omnipod-eros'
|
|||
include ':omnipod-dash'
|
||||
include ':diaconn'
|
||||
|
||||
include ':openhumans'
|
||||
|
|
Loading…
Reference in a new issue