[API] Optimize App Sync a bit.

This commit is contained in:
Kuba Szczodrzyński 2020-03-13 16:22:43 +01:00
parent c568cd3f2e
commit 7c7dff743b
11 changed files with 75 additions and 55 deletions

View File

@ -51,6 +51,7 @@ import okhttp3.RequestBody
import okhttp3.TlsVersion import okhttp3.TlsVersion
import okio.Buffer import okio.Buffer
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Notification
@ -1080,6 +1081,7 @@ fun Throwable.toErrorCode() = when (this) {
private fun ApiResponse.Error.toErrorCode() = when (this.code) { private fun ApiResponse.Error.toErrorCode() = when (this.code) {
else -> ERROR_API_EXCEPTION else -> ERROR_API_EXCEPTION
} }
fun Throwable.toApiError(tag: String) = ApiError.fromThrowable(tag, this)
inline fun <A, B, R> ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? { inline fun <A, B, R> ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? {
if (a != null && b != null) { if (a != null && b != null) {

View File

@ -444,6 +444,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// WHAT'S NEW DIALOG // WHAT'S NEW DIALOG
if (app.config.appVersion < BuildConfig.VERSION_CODE) { if (app.config.appVersion < BuildConfig.VERSION_CODE) {
// force an AppSync after update
app.config.sync.lastAppSync = 0L
ChangelogDialog(this) ChangelogDialog(this)
if (app.config.appVersion < 170) { if (app.config.appVersion < 170) {
//Intent intent = new Intent(this, ChangelogIntroActivity.class); //Intent intent = new Intent(this, ChangelogIntroActivity.class);

View File

@ -105,11 +105,6 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() } get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() }
set(value) { set("widgetConfigs", value); mWidgetConfigs = value } set(value) { set("widgetConfigs", value); mWidgetConfigs = value }
private var mLastAppSync: Long? = null
var lastAppSync: Long
get() { mLastAppSync = mLastAppSync ?: values.get("lastAppSync", 0L); return mLastAppSync ?: 0L }
set(value) { set("lastAppSync", value); mLastAppSync = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow() private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf() private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init { init {

View File

@ -20,6 +20,11 @@ class ConfigSync(private val config: Config) {
get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true } get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true }
set(value) { config.set("syncEnabled", value); mSyncEnabled = value } set(value) { config.set("syncEnabled", value); mSyncEnabled = value }
private var mWebPushEnabled: Boolean? = null
var webPushEnabled: Boolean
get() { mWebPushEnabled = mWebPushEnabled ?: config.values.get("webPushEnabled", true); return mWebPushEnabled ?: true }
set(value) { config.set("webPushEnabled", value); mWebPushEnabled = value }
private var mSyncOnlyWifi: Boolean? = null private var mSyncOnlyWifi: Boolean? = null
var onlyWifi: Boolean var onlyWifi: Boolean
get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates } get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates }
@ -35,6 +40,11 @@ class ConfigSync(private val config: Config) {
get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true } get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true }
set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value } set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value }
private var mLastAppSync: Long? = null
var lastAppSync: Long
get() { mLastAppSync = mLastAppSync ?: config.values.get("lastAppSync", 0L); return mLastAppSync ?: 0L }
set(value) { config.set("lastAppSync", value); mLastAppSync = value }
/* ____ _ _ _ /* ____ _ _ _
/ __ \ (_) | | | | / __ \ (_) | | | |
| | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___ | | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___

View File

@ -22,6 +22,7 @@ import pl.szczodrzynski.edziennik.data.api.task.ErrorReportTask
import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.api.task.SzkolnyTask import pl.szczodrzynski.edziennik.data.api.task.SzkolnyTask
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.toApiError
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -181,7 +182,7 @@ class ApiService : Service() {
is SzkolnyTask -> task.run(taskCallback) is SzkolnyTask -> task.run(taskCallback)
} }
} catch (e: Exception) { } catch (e: Exception) {
taskCallback.onError(ApiError(TAG, EXCEPTION_API_TASK).withThrowable(e)) taskCallback.onError(e.toApiError(TAG))
} }
} }

View File

@ -12,10 +12,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
@ -28,7 +25,6 @@ import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.md5
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
@ -80,7 +76,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
withContext(Dispatchers.Default) { block() } withContext(Dispatchers.Default) { block() }
} }
catch (e: Exception) { catch (e: Exception) {
errorSnackbar.addError(ApiError.fromThrowable(TAG, e)).show() errorSnackbar.addError(e.toApiError(TAG)).show()
null null
} }
} }
@ -91,7 +87,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
catch (e: Exception) { catch (e: Exception) {
ErrorDetailsDialog( ErrorDetailsDialog(
activity, activity,
listOf(ApiError.fromThrowable(TAG, e)), listOf(e.toApiError(TAG)),
R.string.error_occured R.string.error_occured
) )
null null
@ -160,7 +156,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun getEvents(profiles: List<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>): List<EventFull> { fun getEvents(profiles: List<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>, lastSyncTime: Long): List<EventFull> {
val teams = app.db.teamDao().allNow val teams = app.db.teamDao().allNow
val response = api.serverSync(ServerSyncRequest( val response = api.serverSync(ServerSyncRequest(
@ -185,19 +181,26 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
} }
}, },
lastSync = lastSyncTime,
notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) } notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) }
)).execute() )).execute()
parseResponse(response) val (events, hasBrowsers) = parseResponse(response)
val events = mutableListOf<EventFull>() hasBrowsers?.let {
app.config.sync.webPushEnabled = it
}
response.body()?.data?.events?.forEach { event -> val eventList = mutableListOf<EventFull>()
events.forEach { event ->
// skip blacklisted events
if (event.id in blacklistedIds) if (event.id in blacklistedIds)
return@forEach return@forEach
// create the event for every matching team and profile
teams.filter { it.code == event.teamCode }.onEach { team -> teams.filter { it.code == event.teamCode }.onEach { team ->
val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@onEach val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@onEach
events.add(EventFull(event).apply { eventList += EventFull(event).apply {
profileId = team.profileId profileId = team.profileId
teamId = team.id teamId = team.id
addedManually = true addedManually = true
@ -205,11 +208,11 @@ class SzkolnyApi(val app: App) : CoroutineScope {
notified = profile.empty notified = profile.empty
if (profile.userCode == event.sharedBy) sharedBy = "self" if (profile.userCode == event.sharedBy) sharedBy = "self"
}) }
} }
} }
return events return eventList
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -253,9 +256,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
browserId = browserId, browserId = browserId,
pairToken = pairToken pairToken = pairToken
)).execute() )).execute()
parseResponse(response)
return response.body()?.data?.browsers ?: emptyList() return parseResponse(response).browsers
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -265,9 +267,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
device = getDevice(), device = getDevice(),
action = "listBrowsers" action = "listBrowsers"
)).execute() )).execute()
parseResponse(response)
return response.body()?.data?.browsers ?: emptyList() return parseResponse(response).browsers
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -278,9 +279,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
action = "unpairBrowser", action = "unpairBrowser",
browserId = browserId browserId = browserId
)).execute() )).execute()
parseResponse(response)
return response.body()?.data?.browsers ?: emptyList() return parseResponse(response).browsers
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -307,9 +307,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
@Throws(Exception::class) @Throws(Exception::class)
fun getUpdate(channel: String): List<Update> { fun getUpdate(channel: String): List<Update> {
val response = api.updates(channel).execute() val response = api.updates(channel).execute()
parseResponse(response) return parseResponse(response)
return response.body()?.data ?: emptyList()
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -321,8 +319,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
targetDeviceId = targetDeviceId, targetDeviceId = targetDeviceId,
text = text text = text
)).execute() )).execute()
val data = parseResponse(response)
return data.message return parseResponse(response).message
} }
} }

View File

@ -11,6 +11,8 @@ data class ServerSyncRequest(
val userCodes: List<String>, val userCodes: List<String>,
val users: List<User>? = null, val users: List<User>? = null,
val lastSync: Long,
val notifications: List<Notification>? = null val notifications: List<Notification>? = null
) { ) {
data class User( data class User(

View File

@ -6,4 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.response
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
data class ServerSyncResponse(val events: List<EventFull>) data class ServerSyncResponse(
val events: List<EventFull>,
val hasBrowsers: Boolean? = null
)

View File

@ -24,26 +24,25 @@ class AppSync(val app: App, val notifications: MutableList<Notification>, val pr
* *
* @return a number of events inserted to DB, possibly needing a notification * @return a number of events inserted to DB, possibly needing a notification
*/ */
fun run(): Int { fun run(lastSyncTime: Long, markAsSeen: Boolean = false): Int {
val profiles = profiles.filter { it.registration == Profile.REGISTRATION_ENABLED && !it.archived } val blacklistedIds = app.db.eventDao().blacklistedIds
if (profiles.isNotEmpty()) { val events = api.getEvents(profiles, notifications, blacklistedIds, lastSyncTime)
val blacklistedIds = app.db.eventDao().blacklistedIds;
val events = api.getEvents(profiles, notifications, blacklistedIds)
if (events.isNotEmpty()) { app.config.sync.lastAppSync = System.currentTimeMillis()
app.db.metadataDao().addAllIgnore(events.map { event ->
Metadata( if (events.isNotEmpty()) {
event.profileId, app.db.metadataDao().addAllIgnore(events.map { event ->
Metadata.TYPE_EVENT, Metadata(
event.id, event.profileId,
event.seen, Metadata.TYPE_EVENT,
event.notified, event.id,
event.addedDate markAsSeen || event.seen,
) markAsSeen || event.notified,
}) event.addedDate
return app.db.eventDao().addAll(events).size )
} })
return app.db.eventDao().addAll(events).size
} }
return 0; return 0;
} }
} }

View File

@ -31,16 +31,23 @@ class SzkolnyTask(val app: App, val syncingProfiles: List<Profile>) : IApiTask(-
val notifications = Notifications(app, notificationList, profiles) val notifications = Notifications(app, notificationList, profiles)
notifications.run() notifications.run()
val shouldAppSync = notificationList.isNotEmpty() || (System.currentTimeMillis() - app.config.lastAppSync > 24*HOUR*1000) val appSyncProfiles = profiles.filter { it.registration == Profile.REGISTRATION_ENABLED && !it.archived }
// do an AppSync every 24 hours, or if WebPush has a notification // App Sync conditions:
// - every 24 hours && any profile is registered
// - if there are new notifications && any browser is paired
val shouldAppSync =
System.currentTimeMillis() - app.config.sync.lastAppSync > 24*HOUR*1000
&& appSyncProfiles.isNotEmpty()
|| notificationList.isNotEmpty()
&& app.config.sync.webPushEnabled
if (shouldAppSync) { if (shouldAppSync) {
// send notifications to web push, get shared events // send notifications to web push, get shared events
val addedEvents = AppSync(app, notificationList, profiles, api).run() val addedEvents = AppSync(app, notificationList, appSyncProfiles, api).run(app.config.sync.lastAppSync)
if (addedEvents > 0) { if (addedEvents > 0) {
// create notifications for shared events (not present before app sync) // create notifications for shared events (not present before app sync)
notifications.sharedEventNotifications() notifications.sharedEventNotifications()
} }
app.config.lastAppSync = System.currentTimeMillis()
} }
d(TAG, "Created ${notificationList.count()} notifications.") d(TAG, "Created ${notificationList.count()} notifications.")

View File

@ -116,6 +116,8 @@ class WebPushFragment : Fragment(), CoroutineScope {
} }
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
app.config.sync.webPushEnabled = browsers.isNotEmpty()
if (browsers.isNotEmpty()) { if (browsers.isNotEmpty()) {
b.browsersView.visibility = View.VISIBLE b.browsersView.visibility = View.VISIBLE
b.browsersNoData.visibility = View.GONE b.browsersNoData.visibility = View.GONE