Merge pull request #641 from nightscout/openhumans_v2

Update Open Humans uploader for new database
This commit is contained in:
Milos Kozak 2021-09-06 13:54:16 +02:00 committed by GitHub
commit c9897da78d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 2435 additions and 1340 deletions

View file

@ -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')

View file

@ -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>

View file

@ -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

View file

@ -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
}

View file

@ -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> {

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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()
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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 &amp; 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>

View file

@ -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")

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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 {

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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
View file

@ -0,0 +1 @@
/build

15
openhumans/build.gradle Normal file
View 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')
}

View file

21
openhumans/proguard-rules.pro vendored Normal file
View 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

View 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>

View file

@ -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

View file

@ -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")
}
}

View file

@ -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
)

View file

@ -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
}
}

View file

@ -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()
}
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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!!
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View file

@ -16,3 +16,4 @@ include ':omnipod-eros'
include ':omnipod-dash'
include ':diaconn'
include ':openhumans'