diff --git a/app/build.gradle b/app/build.gradle index a7963a3..94a7096 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,12 +41,18 @@ android { } } + compileOptions { + coreLibraryDesugaringEnabled true + } + packagingOptions { resources.excludes += "DebugProbesKt.bin" } } dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation project(":features:home") implementation project(":features:directory") implementation project(":features:login") diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index dd4f8a4..7f7bc4d 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -7,10 +7,7 @@ import android.content.Intent import app.dapk.db.DapkDb import app.dapk.st.BuildConfig import app.dapk.st.SharedPreferencesDelegate -import app.dapk.st.core.BuildMeta -import app.dapk.st.core.CoreAndroidModule -import app.dapk.st.core.CoroutineDispatchers -import app.dapk.st.core.SingletonFlows +import app.dapk.st.core.* import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.directory.DirectoryModule @@ -208,7 +205,7 @@ internal class MatrixModules( installAuthService(credentialsStore) installEncryptionService(store.knownDevicesStore()) - val olmAccountStore = OlmPersistenceWrapper(store.olmStore()) + val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), AndroidBase64()) val singletonFlows = SingletonFlows(coroutineDispatchers) val olm = OlmWrapper( olmStore = olmAccountStore, @@ -417,3 +414,13 @@ class TaskRunnerAdapter(private val matrixTaskRunner: suspend (MatrixTask) -> Ma } } } + +class AndroidBase64 : Base64 { + override fun encode(input: ByteArray): String { + return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT) + } + + override fun decode(input: String): ByteArray { + return android.util.Base64.decode(input, android.util.Base64.DEFAULT) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 6d391bd..d07571a 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ ext.applyCommonAndroidParameters = { project -> incremental = true } android.defaultConfig { - minSdkVersion 29 + minSdkVersion 24 targetSdkVersion androidSdkVersion } diff --git a/core/src/main/kotlin/app/dapk/st/core/Base64.kt b/core/src/main/kotlin/app/dapk/st/core/Base64.kt new file mode 100644 index 0000000..3ad71f3 --- /dev/null +++ b/core/src/main/kotlin/app/dapk/st/core/Base64.kt @@ -0,0 +1,6 @@ +package app.dapk.st.core + +interface Base64 { + fun encode(input: ByteArray): String + fun decode(input: String): ByteArray +} \ No newline at end of file diff --git a/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt b/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt index a1dff08..e60c0ac 100644 --- a/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt +++ b/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt @@ -8,15 +8,15 @@ interface TaskRunner { suspend fun run(tasks: List): List data class RunnableWorkTask( - val source: JobWorkItem, + val source: JobWorkItem?, val task: WorkTask ) sealed interface TaskResult { - val source: JobWorkItem + val source: JobWorkItem? - data class Success(override val source: JobWorkItem) : TaskResult - data class Failure(override val source: JobWorkItem, val canRetry: Boolean) : TaskResult + data class Success(override val source: JobWorkItem?) : TaskResult + data class Failure(override val source: JobWorkItem?, val canRetry: Boolean) : TaskResult } } \ No newline at end of file diff --git a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt index e712235..edb4b38 100644 --- a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt +++ b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt @@ -3,6 +3,7 @@ package app.dapk.st.work import android.app.job.JobParameters import android.app.job.JobService import android.app.job.JobWorkItem +import android.os.Build import app.dapk.st.core.extensions.Scope import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.core.module @@ -24,11 +25,15 @@ class WorkAndroidService : JobService() { when (it) { is TaskRunner.TaskResult.Failure -> { if (!it.canRetry) { - params.completeWork(it.source) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + params.completeWork(it.source!!) + } } } is TaskRunner.TaskResult.Success -> { - params.completeWork(it.source) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + params.completeWork(it.source!!) + } } } } @@ -40,24 +45,37 @@ class WorkAndroidService : JobService() { } private fun JobParameters.collectAllTasks(): List { - var work: JobWorkItem? - val tasks = mutableListOf() - do { - work = this.dequeueWork() - work?.intent?.also { intent -> - tasks.add( - RunnableWorkTask( - source = work, - task = WorkTask( - jobId = this.jobId, - type = intent.getStringExtra("task-type")!!, - jsonPayload = intent.getStringExtra("task-payload")!!, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + var work: JobWorkItem? + val tasks = mutableListOf() + do { + work = this.dequeueWork() + work?.intent?.also { intent -> + tasks.add( + RunnableWorkTask( + source = work, + task = WorkTask( + jobId = this.jobId, + type = intent.getStringExtra("task-type")!!, + jsonPayload = intent.getStringExtra("task-payload")!!, + ) ) ) + } + } while (work != null) + return tasks + } else { + return listOf( + RunnableWorkTask( + source = null, + task = WorkTask( + jobId = this.jobId, + type = this.extras.getString("task-type")!!, + jsonPayload = this.extras.getString("task-payload")!!, + ) ) - } - } while (work != null) - return tasks + ) + } } override fun onStopJob(params: JobParameters): Boolean { diff --git a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt index b4a70ef..ce93d61 100644 --- a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt +++ b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt @@ -6,6 +6,7 @@ import android.app.job.JobWorkItem import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.Build internal class WorkSchedulingJobScheduler( private val context: Context, @@ -23,12 +24,17 @@ internal class WorkSchedulingJobScheduler( .setRequiresDeviceIdle(false) .build() - val item = JobWorkItem( - Intent() - .putExtra("task-type", task.type) - .putExtra("task-payload", task.jsonPayload) - ) - - jobScheduler.enqueue(job, item) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val item = JobWorkItem( + Intent() + .putExtra("task-type", task.type) + .putExtra("task-payload", task.jsonPayload) + ) + jobScheduler.enqueue(job, item) + } else { + job.extras.putString("task-type", task.type) + job.extras.putString("task-payload", task.jsonPayload) + jobScheduler.schedule(job) + } } } diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt index 36869c1..c924186 100644 --- a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt +++ b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt @@ -1,5 +1,6 @@ package app.dapk.st.olm +import app.dapk.st.core.Base64 import app.dapk.st.domain.OlmPersistence import app.dapk.st.domain.SerializedObject import app.dapk.st.matrix.common.Curve25519 @@ -10,10 +11,10 @@ import org.matrix.olm.OlmInboundGroupSession import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmSession import java.io.* -import java.util.* class OlmPersistenceWrapper( private val olmPersistence: OlmPersistence, + private val base64: Base64, ) : OlmStore { override suspend fun read(): OlmAccount? { @@ -49,21 +50,21 @@ class OlmPersistenceWrapper( override suspend fun readInbound(sessionId: SessionId): OlmInboundGroupSession? { return olmPersistence.readInbound(sessionId)?.value?.deserialize() } -} -private fun T.serialize(): String { - val baos = ByteArrayOutputStream() - ObjectOutputStream(baos).use { - it.writeObject(this) + private fun T.serialize(): String { + val baos = ByteArrayOutputStream() + ObjectOutputStream(baos).use { + it.writeObject(this) + } + return base64.encode(baos.toByteArray()) } - return Base64.getEncoder().encode(baos.toByteArray()).toString(Charsets.UTF_8) -} -@Suppress("UNCHECKED_CAST") -private fun String.deserialize(): T { - val decoded = Base64.getDecoder().decode(this) - val baos = ByteArrayInputStream(decoded) - return ObjectInputStream(baos).use { - it.readObject() as T + @Suppress("UNCHECKED_CAST") + private fun String.deserialize(): T { + val decoded = base64.decode(this) + val baos = ByteArrayInputStream(decoded) + return ObjectInputStream(baos).use { + it.readObject() as T + } } } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt index 80e49da..676d968 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt @@ -2,6 +2,7 @@ package app.dapk.st.notifications import android.app.NotificationChannel import android.app.NotificationManager +import android.os.Build private const val channelId = "message" @@ -10,14 +11,16 @@ class NotificationChannels( ) { fun initChannels() { - if (notificationManager.getNotificationChannel(channelId) == null) { - notificationManager.createNotificationChannel( - NotificationChannel( - channelId, - "messages", - NotificationManager.IMPORTANCE_HIGH, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (notificationManager.getNotificationChannel(channelId) == null) { + notificationManager.createNotificationChannel( + NotificationChannel( + channelId, + "messages", + NotificationManager.IMPORTANCE_HIGH, + ) ) - ) + } } } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt index de35316..442b0a6 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt @@ -4,6 +4,8 @@ import android.app.Notification import android.app.PendingIntent import android.app.Person import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi import app.dapk.st.imageloader.IconLoader import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview @@ -46,7 +48,7 @@ class NotificationFactory( } } - return Notification.Builder(context, channelId) + return builder() .setStyle(summaryInboxStyle) .setSmallIcon(R.drawable.ic_notification_small_icon) .setCategory(Notification.CATEGORY_MESSAGE) @@ -55,7 +57,8 @@ class NotificationFactory( .build() } - private suspend fun createNotification(events: List, roomOverview: RoomOverview): NotificationDelegate { + @RequiresApi(Build.VERSION_CODES.P) + private suspend fun createMessageStyle(events: List, roomOverview: RoomOverview): Notification.MessagingStyle { val messageStyle = Notification.MessagingStyle( Person.Builder() .setName("me") @@ -66,7 +69,7 @@ class NotificationFactory( messageStyle.conversationTitle = roomOverview.roomName.takeIf { roomOverview.isGroup } messageStyle.isGroupConversation = roomOverview.isGroup - events.sortedBy { it.utcTimestamp }.forEach { message -> + events.forEach { message -> val sender = Person.Builder() .setName(message.author.displayName ?: message.author.id.value) .setIcon(message.author.avatarUrl?.let { iconLoader.load(it.value) }) @@ -80,6 +83,21 @@ class NotificationFactory( ) ) } + return messageStyle + } + + private suspend fun createNotification(events: List, roomOverview: RoomOverview): NotificationDelegate { + val sortedEvents = events.sortedBy { it.utcTimestamp } + + val messageStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + createMessageStyle(sortedEvents, roomOverview) + } else { + val inboxStyle = Notification.InboxStyle() + events.forEach { + inboxStyle.addLine("${it.author.displayName ?: it.author.id.value}: ${it.content}") + } + inboxStyle + } val openRoomIntent = PendingIntent.getActivity( context, @@ -89,25 +107,37 @@ class NotificationFactory( ) return NotificationDelegate.Room( - Notification.Builder(context, channelId) - .setWhen(messageStyle.messages.last().timestamp) + builder() + .setWhen(sortedEvents.last().utcTimestamp) .setShowWhen(true) .setGroup(GROUP_ID) .setOnlyAlertOnce(roomOverview.isGroup) .setContentIntent(openRoomIntent) .setStyle(messageStyle) .setCategory(Notification.CATEGORY_MESSAGE) - .setShortcutId(roomOverview.roomId.value) + .run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.setShortcutId(roomOverview.roomId.value) + } else { + this + } + } .setSmallIcon(R.drawable.ic_notification_small_icon) .setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) }) .setAutoCancel(true) .build(), roomId = roomOverview.roomId, - summary = messageStyle.messages.last().text.toString() + summary = events.last().content ) } + private fun builder() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Notification.Builder(context, channelId) + } else { + Notification.Builder(context) + } + } data class Notifications(val summaryNotification: Notification?, val delegates: List) \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index c5de509..d38e91f 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -1,6 +1,7 @@ package test import TestUser +import app.dapk.st.core.Base64 import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.SingletonFlows import app.dapk.st.domain.StoreModule @@ -81,7 +82,7 @@ class TestMatrix( installAuthService(storeModule.credentialsStore(), AuthConfig(forceHttp = false)) installEncryptionService(storeModule.knownDevicesStore()) - val olmAccountStore = OlmPersistenceWrapper(storeModule.olmStore()) + val olmAccountStore = OlmPersistenceWrapper(storeModule.olmStore(), JavaBase64()) val olm = OlmWrapper( olmStore = olmAccountStore, singletonFlows = SingletonFlows(coroutineDispatchers), @@ -271,3 +272,13 @@ class TestMatrix( suspend fun deviceId() = storeModule.credentialsStore().credentials()!!.deviceId suspend fun userId() = storeModule.credentialsStore().credentials()!!.userId } + +class JavaBase64 : Base64 { + override fun encode(input: ByteArray): String { + return java.util.Base64.getEncoder().encode(input).toString(Charsets.UTF_8) + } + + override fun decode(input: String): ByteArray { + return java.util.Base64.getDecoder().decode(input) + } +} \ No newline at end of file