oh foreground worker and some edgecases
This commit is contained in:
parent
8a6a3d2283
commit
ae980e12f4
5 changed files with 68 additions and 15 deletions
|
@ -1,12 +1,19 @@
|
||||||
package info.nightscout.androidaps.plugins.general.openhumans
|
package info.nightscout.androidaps.plugins.general.openhumans
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.RxWorker
|
import androidx.work.RxWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import info.nightscout.androidaps.MainApp
|
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 info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class OHUploadWorker(context: Context, workerParameters: WorkerParameters)
|
class OHUploadWorker(context: Context, workerParameters: WorkerParameters)
|
||||||
|
@ -18,6 +25,9 @@ class OHUploadWorker(context: Context, workerParameters: WorkerParameters)
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var openHumansUploader: OpenHumansUploader
|
lateinit var openHumansUploader: OpenHumansUploader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var resourceHelper: ResourceHelper
|
||||||
|
|
||||||
override fun createWork(): Single<Result> = Single.defer {
|
override fun createWork(): Single<Result> = Single.defer {
|
||||||
|
|
||||||
// Here we inject every time we create work
|
// Here we inject every time we create work
|
||||||
|
@ -31,7 +41,8 @@ class OHUploadWorker(context: Context, workerParameters: WorkerParameters)
|
||||||
val wifiOnly = sp.getBoolean("key_oh_wifi_only", true)
|
val wifiOnly = sp.getBoolean("key_oh_wifi_only", true)
|
||||||
val isConnectedToWifi = wifiManager?.isWifiEnabled ?: false && wifiManager?.connectionInfo?.networkId != -1
|
val isConnectedToWifi = wifiManager?.isWifiEnabled ?: false && wifiManager?.connectionInfo?.networkId != -1
|
||||||
if (!wifiOnly || (wifiOnly && isConnectedToWifi)) {
|
if (!wifiOnly || (wifiOnly && isConnectedToWifi)) {
|
||||||
openHumansUploader.uploadData()
|
setForegroundAsync(createForegroundInfo())
|
||||||
|
openHumansUploader.uploadData().delay(12, TimeUnit.MINUTES) //TODO OH: No Delay
|
||||||
.andThen(Single.just(Result.success()))
|
.andThen(Single.just(Result.success()))
|
||||||
.onErrorResumeNext { Single.just(Result.retry()) }
|
.onErrorResumeNext { Single.just(Result.retry()) }
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,4 +50,17 @@ class OHUploadWorker(context: Context, workerParameters: WorkerParameters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.setContentTitle(resourceHelper.gs(info.nightscout.androidaps.R.string.open_humans))
|
||||||
|
.setSmallIcon(info.nightscout.androidaps.R.drawable.notif_icon)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
return ForegroundInfo(UPLOAD_NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,7 +7,6 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
@ -16,7 +15,9 @@ import info.nightscout.androidaps.MainApp
|
||||||
import info.nightscout.androidaps.R
|
import info.nightscout.androidaps.R
|
||||||
import info.nightscout.androidaps.events.Event
|
import info.nightscout.androidaps.events.Event
|
||||||
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
||||||
|
import info.nightscout.androidaps.utils.alertDialogs.OKDialog
|
||||||
import info.nightscout.androidaps.utils.extensions.plusAssign
|
import info.nightscout.androidaps.utils.extensions.plusAssign
|
||||||
|
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
@ -40,6 +41,9 @@ class OpenHumansFragment : DaggerFragment() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var openHumansUploader: OpenHumansUploader
|
lateinit var openHumansUploader: OpenHumansUploader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var resourceHelper: ResourceHelper
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
compositeDisposable += rxBus.toObservable(UpdateQueueEvent::class.java)
|
compositeDisposable += rxBus.toObservable(UpdateQueueEvent::class.java)
|
||||||
|
@ -85,7 +89,9 @@ class OpenHumansFragment : DaggerFragment() {
|
||||||
queueSize = view.findViewById(R.id.queue_size)
|
queueSize = view.findViewById(R.id.queue_size)
|
||||||
workerState = view.findViewById(R.id.worker_state)
|
workerState = view.findViewById(R.id.worker_state)
|
||||||
login!!.setOnClickListener { startActivity(Intent(context, OpenHumansLoginActivity::class.java)) }
|
login!!.setOnClickListener { startActivity(Intent(context, OpenHumansLoginActivity::class.java)) }
|
||||||
logout!!.setOnClickListener { openHumansUploader.logout() }
|
logout!!.setOnClickListener {
|
||||||
|
activity?.let { activity -> OKDialog.showConfirmation(activity, resourceHelper.gs(R.string.oh_logout_confirmation), Runnable { openHumansUploader.logout() }) }
|
||||||
|
}
|
||||||
viewsCreated = true
|
viewsCreated = true
|
||||||
updateGUI()
|
updateGUI()
|
||||||
return view
|
return view
|
||||||
|
|
|
@ -24,6 +24,7 @@ import info.nightscout.androidaps.interfaces.PluginType
|
||||||
import info.nightscout.androidaps.logging.AAPSLogger
|
import info.nightscout.androidaps.logging.AAPSLogger
|
||||||
import info.nightscout.androidaps.logging.LTag
|
import info.nightscout.androidaps.logging.LTag
|
||||||
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
|
||||||
|
import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin
|
||||||
import info.nightscout.androidaps.utils.extensions.plusAssign
|
import info.nightscout.androidaps.utils.extensions.plusAssign
|
||||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||||
|
@ -53,7 +54,8 @@ class OpenHumansUploader @Inject constructor(
|
||||||
aapsLogger: AAPSLogger,
|
aapsLogger: AAPSLogger,
|
||||||
val sp: SP,
|
val sp: SP,
|
||||||
val rxBus: RxBusWrapper,
|
val rxBus: RxBusWrapper,
|
||||||
val context: Context
|
val context: Context,
|
||||||
|
val treatmentsPlugin: TreatmentsPlugin
|
||||||
) : PluginBase(
|
) : PluginBase(
|
||||||
PluginDescription()
|
PluginDescription()
|
||||||
.mainType(PluginType.GENERAL)
|
.mainType(PluginType.GENERAL)
|
||||||
|
@ -72,10 +74,12 @@ class OpenHumansUploader @Inject constructor(
|
||||||
private const val REDIRECT_URL = "androidaps://setup-openhumans"
|
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 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 WORK_NAME = "Open Humans"
|
||||||
|
const val NOTIFICATION_CHANNEL = "OpenHumans"
|
||||||
private const val COPY_NOTIFICATION_ID = 3122
|
private const val COPY_NOTIFICATION_ID = 3122
|
||||||
private const val FAILURE_NOTIFICATION_ID = 3123
|
private const val FAILURE_NOTIFICATION_ID = 3123
|
||||||
private const val SUCCESS_NOTIFICATION_ID = 3124
|
private const val SUCCESS_NOTIFICATION_ID = 3124
|
||||||
private const val SIGNED_OUT_NOTIFICATION_ID = 3125
|
private const val SIGNED_OUT_NOTIFICATION_ID = 3125
|
||||||
|
const val UPLOAD_NOTIFICATION_ID = 3126
|
||||||
}
|
}
|
||||||
|
|
||||||
private val openHumansAPI = OpenHumansAPI(OPEN_HUMANS_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URL)
|
private val openHumansAPI = OpenHumansAPI(OPEN_HUMANS_URL, CLIENT_ID, CLIENT_SECRET, REDIRECT_URL)
|
||||||
|
@ -323,7 +327,7 @@ class OpenHumansUploader @Inject constructor(
|
||||||
.flatMap { openHumansAPI.getProjectMemberId(it.accessToken) }
|
.flatMap { openHumansAPI.getProjectMemberId(it.accessToken) }
|
||||||
.doOnSuccess {
|
.doOnSuccess {
|
||||||
projectMemberId = it
|
projectMemberId = it
|
||||||
// TODO: halted for now. Might create too much upload data. copyExistingDataToQueue()
|
copyExistingDataToQueue()
|
||||||
rxBus.send(OpenHumansFragment.UpdateViewEvent)
|
rxBus.send(OpenHumansFragment.UpdateViewEvent)
|
||||||
}
|
}
|
||||||
.doOnError {
|
.doOnError {
|
||||||
|
@ -353,7 +357,10 @@ class OpenHumansUploader @Inject constructor(
|
||||||
copyDisposable = Completable.fromCallable { MainApp.getDbHelper().clearOpenHumansQueue() }
|
copyDisposable = Completable.fromCallable { MainApp.getDbHelper().clearOpenHumansQueue() }
|
||||||
.andThen(Single.defer { Single.just(MainApp.getDbHelper().countOfAllRows) })
|
.andThen(Single.defer { Single.just(MainApp.getDbHelper().countOfAllRows) })
|
||||||
.doOnSuccess { maxProgress = it }
|
.doOnSuccess { maxProgress = it }
|
||||||
.flatMapObservable { Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allBgReadings) } }
|
.flatMapObservable { Observable.defer { Observable.fromIterable(treatmentsPlugin.service.treatmentData) } }
|
||||||
|
.map { enqueueTreatment(it); increaseCounter() }
|
||||||
|
.ignoreElements()
|
||||||
|
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allBgReadings) })
|
||||||
.map { enqueueBGReading(it); increaseCounter() }
|
.map { enqueueBGReading(it); increaseCounter() }
|
||||||
.ignoreElements()
|
.ignoreElements()
|
||||||
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allCareportalEvents) })
|
.andThen(Observable.defer { Observable.fromIterable(MainApp.getDbHelper().allCareportalEvents) })
|
||||||
|
@ -384,6 +391,7 @@ class OpenHumansUploader @Inject constructor(
|
||||||
showSetupFinishedNotification()
|
showSetupFinishedNotification()
|
||||||
}
|
}
|
||||||
.doOnError {
|
.doOnError {
|
||||||
|
logout()
|
||||||
showSetupFailedNotification()
|
showSetupFailedNotification()
|
||||||
}
|
}
|
||||||
.doFinally {
|
.doFinally {
|
||||||
|
@ -397,7 +405,7 @@ class OpenHumansUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showOngoingNotification(maxProgress: Long? = null, currentProgress: Long? = null) {
|
private fun showOngoingNotification(maxProgress: Long? = null, currentProgress: Long? = null) {
|
||||||
val notification = NotificationCompat.Builder(context, "OpenHumans")
|
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||||
.setContentTitle(resourceHelper.gs(R.string.finishing_open_humans_setup))
|
.setContentTitle(resourceHelper.gs(R.string.finishing_open_humans_setup))
|
||||||
.setContentText(resourceHelper.gs(R.string.this_may_take_a_while))
|
.setContentText(resourceHelper.gs(R.string.this_may_take_a_while))
|
||||||
.setStyle(NotificationCompat.BigTextStyle())
|
.setStyle(NotificationCompat.BigTextStyle())
|
||||||
|
@ -411,9 +419,9 @@ class OpenHumansUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSetupFinishedNotification() {
|
private fun showSetupFinishedNotification() {
|
||||||
val notification = NotificationCompat.Builder(context, "OpenHumans")
|
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||||
.setContentTitle(resourceHelper.gs(R.string.setup_finished))
|
.setContentTitle(resourceHelper.gs(R.string.setup_finished))
|
||||||
.setContentText(resourceHelper.gs(R.string.your_phone_is_upload_data))
|
.setContentText(resourceHelper.gs(R.string.your_phone_will_upload_data))
|
||||||
.setStyle(NotificationCompat.BigTextStyle())
|
.setStyle(NotificationCompat.BigTextStyle())
|
||||||
.setSmallIcon(R.drawable.notif_icon)
|
.setSmallIcon(R.drawable.notif_icon)
|
||||||
.build()
|
.build()
|
||||||
|
@ -422,7 +430,7 @@ class OpenHumansUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSetupFailedNotification() {
|
private fun showSetupFailedNotification() {
|
||||||
val notification = NotificationCompat.Builder(context, "OpenHumans")
|
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||||
.setContentTitle(resourceHelper.gs(R.string.setup_failed))
|
.setContentTitle(resourceHelper.gs(R.string.setup_failed))
|
||||||
.setContentText(resourceHelper.gs(R.string.there_was_an_error))
|
.setContentText(resourceHelper.gs(R.string.there_was_an_error))
|
||||||
.setStyle(NotificationCompat.BigTextStyle())
|
.setStyle(NotificationCompat.BigTextStyle())
|
||||||
|
@ -559,7 +567,7 @@ class OpenHumansUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSignOut() {
|
private fun handleSignOut() {
|
||||||
val notification = NotificationCompat.Builder(context, "OpenHumans")
|
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL)
|
||||||
.setContentTitle(resourceHelper.gs(R.string.you_have_been_signed_out_of_open_humans))
|
.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))
|
.setContentText(resourceHelper.gs(R.string.click_here_to_sign_in_again_if_this_wasnt_on_purpose))
|
||||||
.setStyle(NotificationCompat.BigTextStyle())
|
.setStyle(NotificationCompat.BigTextStyle())
|
||||||
|
@ -587,9 +595,9 @@ class OpenHumansUploader @Inject constructor(
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.setRequiresCharging(sp.getBoolean("key_oh_charging_only", false))
|
.setRequiresCharging(sp.getBoolean("key_oh_charging_only", false))
|
||||||
.build()
|
.build()
|
||||||
val workRequest = PeriodicWorkRequestBuilder<OHUploadWorker>(1, TimeUnit.DAYS)
|
val workRequest = PeriodicWorkRequestBuilder<OHUploadWorker>(1, TimeUnit.MINUTES) // TODO OH: DAYS
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.HOURS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES) //TODO OH: HOURS
|
||||||
.build()
|
.build()
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_NAME, if (replace) ExistingPeriodicWorkPolicy.REPLACE else ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_NAME, if (replace) ExistingPeriodicWorkPolicy.REPLACE else ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
||||||
}
|
}
|
||||||
|
@ -598,7 +606,7 @@ class OpenHumansUploader @Inject constructor(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val notificationManagerCompat = NotificationManagerCompat.from(context)
|
val notificationManagerCompat = NotificationManagerCompat.from(context)
|
||||||
notificationManagerCompat.createNotificationChannel(NotificationChannel(
|
notificationManagerCompat.createNotificationChannel(NotificationChannel(
|
||||||
"OpenHumans",
|
NOTIFICATION_CHANNEL,
|
||||||
resourceHelper.gs(R.string.open_humans),
|
resourceHelper.gs(R.string.open_humans),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
))
|
))
|
||||||
|
|
|
@ -160,6 +160,10 @@ public class TreatmentService extends OrmLiteBaseService<DatabaseHelper> {
|
||||||
public List<Treatment> query(PreparedQuery<Treatment> data) throws SQLException {
|
public List<Treatment> query(PreparedQuery<Treatment> data) throws SQLException {
|
||||||
return wrapped.query(data);
|
return wrapped.query(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long countOf() throws SQLException {
|
||||||
|
return wrapped.countOf();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -295,6 +299,15 @@ public class TreatmentService extends OrmLiteBaseService<DatabaseHelper> {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long count() {
|
||||||
|
try {
|
||||||
|
return this.getDao().countOf();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
aapsLogger.error("Unhandled exception", e);
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
"_id": "551ee3ad368e06e80856e6a9",
|
"_id": "551ee3ad368e06e80856e6a9",
|
||||||
|
|
|
@ -1411,13 +1411,15 @@
|
||||||
<string name="finishing_open_humans_setup">Finishing Open Humans setup…</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 off.</string>
|
<string name="this_may_take_a_while">This may take a while. Do not turn your phone off.</string>
|
||||||
<string name="setup_finished">Setup finished</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="your_phone_is_upload_data">Your phone is uploading data to Open Humans now.</string>
|
||||||
<string name="setup_failed">Setup failed</string>
|
<string name="setup_failed">Setup failed</string>
|
||||||
<string name="there_was_an_error">There was an error.</string>
|
<string name="there_was_an_error">There was an error. Please try to log in again in order to proceed. Sorry & Thank you!</string>
|
||||||
<string name="open_humans_terms">This is an open source tool that will copy your data to Open Humans. We retain no rights to share your data with third parties without your explicit authorization. The data the project and app receive are identified via a random user ID and will only be securely transmitted to an Open Humans account with your authorization of that process. You can stop uploading and delete your upload data at any time via www.openhumans.org.</string>
|
<string name="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="i_understand_and_agree">I understand and agree.</string>
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
<string name="logout">Logout</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="project_member_id">Project Member ID: %s</string>
|
||||||
<string name="queue_size">Queue Size: %d</string>
|
<string name="queue_size">Queue Size: %d</string>
|
||||||
<string name="terms_of_use">Terms of Use</string>
|
<string name="terms_of_use">Terms of Use</string>
|
||||||
|
|
Loading…
Reference in a new issue