diff --git a/app/build.gradle b/app/build.gradle index 11c43b78..c57a50f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,6 +170,9 @@ dependencies { implementation 'com.github.kuba2k2:RecyclerTabLayout:700f980584' implementation 'com.github.kuba2k2:Tachyon:551943a6b5' + + implementation "com.squareup.retrofit2:retrofit:${versions.retrofit}" + implementation "com.squareup.retrofit2:converter-gson:${versions.retrofit}" } repositories { mavenCentral() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.java b/app/src/main/java/pl/szczodrzynski/edziennik/App.java index 0b62324b..2ca40b3c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.java @@ -222,7 +222,7 @@ public class App extends androidx.multidex.MultiDexApplication implements Config byte[] signatureBytes = signature.toByteArray(); MessageDigest md = MessageDigest.getInstance("SHA"); md.update(signatureBytes); - this.signature = Base64.encodeToString(md.digest(), Base64.DEFAULT); + this.signature = Base64.encodeToString(md.digest(), Base64.NO_WRAP); //Log.d(TAG, "Signature is "+this.signature); } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index 3ac87027..bbc83bdb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -15,6 +15,8 @@ import android.text.* import android.text.style.ForegroundColorSpan import android.text.style.StrikethroughSpan import android.text.style.StyleSpan +import android.util.Base64.NO_WRAP +import android.util.Base64.encodeToString import android.util.LongSparseArray import android.util.SparseArray import android.util.TypedValue @@ -34,15 +36,20 @@ import im.wangchao.mhttp.Response import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import okhttp3.RequestBody +import okio.Buffer import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher import pl.szczodrzynski.edziennik.data.db.modules.teams.Team import pl.szczodrzynski.edziennik.utils.models.Time -import pl.szczodrzynski.navlib.getColorFromAttr import pl.szczodrzynski.navlib.getColorFromRes +import java.math.BigInteger +import java.security.MessageDigest import java.text.SimpleDateFormat import java.util.* import java.util.zip.CRC32 +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec fun List.byId(id: Long) = firstOrNull { it.id == id } @@ -358,6 +365,28 @@ fun String.crc32(): Long { return crc.value } +fun String.hmacSHA1(password: String): String { + val key = SecretKeySpec(password.toByteArray(), "HmacSHA1") + + val mac = Mac.getInstance("HmacSHA1").apply { + init(key) + update(this@hmacSHA1.toByteArray()) + } + + return encodeToString(mac.doFinal(), NO_WRAP) +} + +fun String.md5(): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0') +} + +fun RequestBody.bodyToString(): String { + val buffer = Buffer() + writeTo(buffer) + return buffer.readUtf8() +} + fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this) fun CharSequence?.asColoredSpannable(colorInt: Int): Spannable { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/ApiService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/ApiService.kt index 6a5bde01..9944f701 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/ApiService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/ApiService.kt @@ -15,12 +15,10 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.api.v2.events.* import pl.szczodrzynski.edziennik.api.v2.events.requests.ServiceCloseRequest import pl.szczodrzynski.edziennik.api.v2.events.requests.TaskCancelRequest -import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask -import pl.szczodrzynski.edziennik.api.v2.events.task.ErrorReportTask -import pl.szczodrzynski.edziennik.api.v2.events.task.IApiTask -import pl.szczodrzynski.edziennik.api.v2.events.task.NotifyTask +import pl.szczodrzynski.edziennik.api.v2.events.task.* import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.api.v2.models.ApiError +import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull import pl.szczodrzynski.edziennik.utils.Utils.d import kotlin.math.min import kotlin.math.roundToInt @@ -41,8 +39,8 @@ class ApiService : Service() { private val app by lazy { applicationContext as App } private val finishingTaskQueue = mutableListOf( - NotifyTask(), - ErrorReportTask() + ServerSyncTask(), + NotifyTask() ) private val taskQueue = mutableListOf() private val errorList = mutableListOf() @@ -63,6 +61,8 @@ class ApiService : Service() { private var lastEventTime = System.currentTimeMillis() private var taskCancelTries = 0 + private val syncingProfiles = mutableListOf() + /* ______ _ _ _ _ _____ _ _ _ _ | ____| | | (_) (_) | / ____| | | | | | | | |__ __| |_____ ___ _ __ _ __ _| | __ | | __ _| | | |__ __ _ ___| | __ @@ -156,11 +156,14 @@ class ApiService : Service() { // post an event EventBus.getDefault().post(ApiTaskStartedEvent(taskProfileId, task.profile)) + task.profile?.let { syncingProfiles.add(it) } + try { when (task) { is EdziennikTask -> task.run(app, taskCallback) is NotifyTask -> task.run(app, taskCallback) is ErrorReportTask -> task.run(app, taskCallback, notification, errorList) + is ServerSyncTask -> task.run(app, syncingProfiles, taskCallback) } } catch (e: Exception) { taskCallback.onError(ApiError(TAG, EXCEPTION_API_TASK).withThrowable(e)) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/EdziennikTask.kt index 46ee096a..253ef311 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/EdziennikTask.kt @@ -39,7 +39,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa taskName = app.getString(R.string.edziennik_notification_api_first_login_title) } else { // get the requested profile and login store - val profile = app.db.profileDao().getByIdNow(profileId) + val profile = app.db.profileDao().getFullByIdNow(profileId) this.profile = profile if (profile == null) { return diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/IApiTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/IApiTask.kt index df3fa33a..060409f6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/IApiTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/IApiTask.kt @@ -11,11 +11,11 @@ import android.os.Build.VERSION_CODES.O import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.api.v2.ApiService -import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile +import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull abstract class IApiTask(open val profileId: Int) { var taskId: Int = 0 - var profile: Profile? = null + var profile: ProfileFull? = null var taskName: String? = null /** @@ -39,4 +39,4 @@ abstract class IApiTask(open val profileId: Int) { override fun toString(): String { return "IApiTask(profileId=$profileId, taskId=$taskId, profile=$profile, taskName=$taskName)" } -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/ServerSyncTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/ServerSyncTask.kt index 90bfee83..49ded1dc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/ServerSyncTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/events/task/ServerSyncTask.kt @@ -7,6 +7,9 @@ package pl.szczodrzynski.edziennik.api.v2.events.task import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.api.v2.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata +import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull class ServerSyncTask : IApiTask(-1) { override fun prepare(app: App) { @@ -17,9 +20,25 @@ class ServerSyncTask : IApiTask(-1) { } - fun run(app: App, taskCallback: EdziennikCallback) { + fun run(app: App, profiles: List, taskCallback: EdziennikCallback) { + val api = SzkolnyApi(app, profiles) + val events = api.getEvents() + + if (events.isNotEmpty()) { + app.db.eventDao().addAll(events) + app.db.metadataDao().addAllIgnore(events.map { event -> + Metadata( + event.profileId, + Metadata.TYPE_EVENT, + event.id, + event.seen, + event.notified, + event.addedDate + ) + }) + } taskCallback.onCompleted() } -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/SzkolnyApi.kt new file mode 100644 index 00000000..e583c36a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/SzkolnyApi.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.api.v2.szkolny + +import android.os.Build +import com.google.gson.GsonBuilder +import okhttp3.OkHttpClient +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.api.v2.szkolny.adapter.DateAdapter +import pl.szczodrzynski.edziennik.api.v2.szkolny.adapter.TimeAdapter +import pl.szczodrzynski.edziennik.api.v2.szkolny.interceptor.SignatureInterceptor +import pl.szczodrzynski.edziennik.api.v2.szkolny.request.ServerSyncRequest +import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull +import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.create +import java.util.concurrent.TimeUnit.SECONDS + +class SzkolnyApi(val app: App, val profiles: List) { + + private var api: SzkolnyService + + init { + val okHttpClient: OkHttpClient = app.http.newBuilder() + .followRedirects(true) + .callTimeout(30, SECONDS) + .addInterceptor(SignatureInterceptor(app)) + .build() + + val gsonConverterFactory = GsonConverterFactory.create( + GsonBuilder() + .setLenient() + .registerTypeAdapter(Date::class.java, DateAdapter()) + .registerTypeAdapter(Time::class.java, TimeAdapter()) + .create()) + + val retrofit: Retrofit = Retrofit.Builder() + .baseUrl("https://api.szkolny.eu/") + .addConverterFactory(gsonConverterFactory) + .client(okHttpClient) + .build() + + api = retrofit.create() + } + + fun getEvents(): List { + val teams = app.db.teamDao().allNow + + val response = api.serverSync(ServerSyncRequest( + deviceId = app.deviceId, + device = ServerSyncRequest.Device( + osType = "Android", + osVersion = Build.VERSION.RELEASE, + hardware = "${Build.MANUFACTURER} ${Build.MODEL}", + pushToken = app.config.sync.tokenApp, + appVersion = BuildConfig.VERSION_NAME, + appType = BuildConfig.BUILD_TYPE, + appVersionCode = BuildConfig.VERSION_CODE, + syncInterval = app.config.sync.interval + ), + userCodes = profiles.map { it.usernameId }, + users = profiles.map { profile -> + ServerSyncRequest.User( + profile.usernameId, + profile.studentNameLong ?: "", + profile.studentNameShort ?: "", + profile.loginStoreType, + teams.filter { it.profileId == profile.id }.map { it.code } + ) + } + )).execute().body() + + val events = mutableListOf() + + response?.data?.events?.forEach { event -> + teams.filter { it.code == event.teamCode }.forEach { team -> + val profile = profiles.firstOrNull { it.id == team.profileId } + + events.add(event.apply { + profileId = team.profileId + teamId = team.id + addedManually = true + seen = profile?.empty ?: false + notified = profile?.empty ?: false + + if (profile?.usernameId == event.sharedBy) sharedBy = "self" + }) + } + } + + return events + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/SzkolnyService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/SzkolnyService.kt new file mode 100644 index 00000000..a7c08ba7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/SzkolnyService.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.api.v2.szkolny + +import pl.szczodrzynski.edziennik.api.v2.szkolny.request.ServerSyncRequest +import pl.szczodrzynski.edziennik.api.v2.szkolny.response.ApiResponse +import pl.szczodrzynski.edziennik.api.v2.szkolny.response.ServerSyncResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface SzkolnyService { + + @POST("appSync") + fun serverSync(@Body request: ServerSyncRequest): Call> +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/adapter/DateAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/adapter/DateAdapter.kt new file mode 100644 index 00000000..33081240 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/adapter/DateAdapter.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.api.v2.szkolny.adapter + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import pl.szczodrzynski.edziennik.utils.models.Date + +class DateAdapter : TypeAdapter() { + override fun write(writer: JsonWriter?, value: Date?) {} + + override fun read(reader: JsonReader?): Date? { + if (reader?.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + return reader?.nextInt()?.let { Date.fromValue(it) } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/adapter/TimeAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/adapter/TimeAdapter.kt new file mode 100644 index 00000000..f495fbc8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/szkolny/adapter/TimeAdapter.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-8 + */ + +package pl.szczodrzynski.edziennik.api.v2.szkolny.adapter + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import pl.szczodrzynski.edziennik.utils.models.Time + +class TimeAdapter : TypeAdapter