Browse Source

Merge 93c50dd271 into 598ef04910

pull/5040/merge
Konrad Pozniak 10 months ago committed by GitHub
parent
commit
db0b6d84c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      app/build.gradle
  2. 4
      app/proguard-rules.pro
  3. 15
      app/src/main/AndroidManifest.xml
  4. 4
      app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
  5. 12
      app/src/main/java/com/keylesspalace/tusky/MainViewModel.kt
  6. 1
      app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt
  7. 3
      app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt
  8. 6
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt
  9. 4
      app/src/main/java/com/keylesspalace/tusky/components/notifications/SeveredRelationshipNotificationViewHolder.kt
  10. 10
      app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
  11. 6
      app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt
  12. 85
      app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.kt
  13. 2
      app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
  14. 4
      app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt
  15. 6
      app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt
  16. 3
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
  17. 8
      app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt
  18. 22
      app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
  19. 45
      app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushService.kt
  20. 2
      app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
  21. 6
      app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt
  22. 60
      app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt
  23. 8
      app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt
  24. 8
      app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt
  25. 10
      app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
  26. 7
      gradle/libs.versions.toml
  27. 83
      gradle/verification-metadata.xml

3
app/build.gradle

@ -187,8 +187,7 @@ dependencies {
implementation libs.bundles.filemojicompat
implementation libs.bouncycastle
implementation libs.unified.push
implementation libs.bundles.unifiedpush
implementation libs.bundles.xmldiff

4
app/proguard-rules.pro vendored

@ -26,10 +26,6 @@
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
# Bouncy Castle -- Keep EC
-keep class org.bouncycastle.jcajce.provider.asymmetric.EC$* { *; }
-keep class org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi$EC
# Preference fragments can be referenced by name, ensure they remain
# https://github.com/tuskyapp/Tusky/issues/3161
-keep class * extends androidx.preference.PreferenceFragmentCompat

15
app/src/main/AndroidManifest.xml

@ -159,19 +159,12 @@
android:name=".receiver.SendStatusBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:exported="true"
android:enabled="true"
android:name=".receiver.UnifiedPushBroadcastReceiver"
tools:ignore="ExportedReceiver">
<service android:name=".receiver.UnifiedPushService"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
</intent-filter>
</receiver>
</service>
<receiver
android:exported="true"
android:enabled="true"

4
app/src/main/java/com/keylesspalace/tusky/MainActivity.kt

@ -82,7 +82,7 @@ import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.components.trending.TrendingActivity
import com.keylesspalace.tusky.databinding.ActivityMainBinding
import com.keylesspalace.tusky.db.DraftsAlert
@ -137,7 +137,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
lateinit var eventHub: EventHub
@Inject
lateinit var notificationService: NotificationService
lateinit var notificationHelper: NotificationHelper
@Inject
lateinit var cacheUpdater: CacheUpdater

12
app/src/main/java/com/keylesspalace/tusky/MainViewModel.kt

@ -24,7 +24,7 @@ import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.NewNotificationsEvent
import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Notification
@ -48,7 +48,7 @@ class MainViewModel @Inject constructor(
private val eventHub: EventHub,
private val accountManager: AccountManager,
private val shareShortcutHelper: ShareShortcutHelper,
private val notificationService: NotificationService,
private val notificationHelper: NotificationHelper,
) : ViewModel() {
private val activeAccount = accountManager.activeAccount!!
@ -160,15 +160,15 @@ class MainViewModel @Inject constructor(
// notifications fully disabled) will get unnoticed; and also an app restart cannot be easily triggered by the user.
// TODO it's quite odd to separate channel creation (for an account) from the "is enabled by channels" question below
notificationService.createNotificationChannelsForAccount(activeAccount)
notificationHelper.createNotificationChannelsForAccount(activeAccount)
if (notificationService.areNotificationsEnabledBySystem()) {
if (notificationHelper.areNotificationsEnabledBySystem()) {
viewModelScope.launch {
notificationService.setupNotifications(activity)
notificationHelper.setupNotifications(activity)
}
} else {
viewModelScope.launch {
notificationService.disableAllNotifications()
notificationHelper.disableAllNotifications()
}
}
}

1
app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt

@ -32,4 +32,5 @@ data class InstanceInfo(
val version: String?,
val translationEnabled: Boolean?,
val mastodonApiVersion: Int?,
val vapidKey: String?
)

3
app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt

@ -147,6 +147,7 @@ class InstanceInfoRepository @Inject constructor(
version = this?.version,
translationEnabled = this?.translationEnabled,
mastodonApiVersion = this?.mastodonApiVersion,
vapidKey = this?.vapidKey
)
private fun Instance.toEntity() = InstanceInfoEntity(
@ -176,6 +177,7 @@ class InstanceInfoRepository @Inject constructor(
maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength,
translationEnabled = this.configuration?.translation?.enabled,
mastodonApiVersion = this.apiVersions?.mastodon,
vapidKey = this.configuration?.vapid?.publicKey
)
private fun InstanceV1.toEntity(instanceName: String) =
@ -205,6 +207,7 @@ class InstanceInfoRepository @Inject constructor(
maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength,
translationEnabled = null,
mastodonApiVersion = null,
vapidKey = null
)
companion object {

6
app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt

@ -53,7 +53,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.notifications.requests.NotificationRequestsActivity
import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding
import com.keylesspalace.tusky.databinding.NotificationsFilterBinding
import com.keylesspalace.tusky.entity.Status
@ -100,7 +100,7 @@ class NotificationsFragment :
lateinit var eventHub: EventHub
@Inject
lateinit var notificationService: NotificationService
lateinit var notificationHelper: NotificationHelper
private val binding by viewBinding(FragmentTimelineNotificationsBinding::bind)
@ -269,7 +269,7 @@ class NotificationsFragment :
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
accountManager.activeAccount?.let { account ->
notificationService.clearNotificationsForAccount(account)
notificationHelper.clearNotificationsForAccount(account)
}
}
}

4
app/src/main/java/com/keylesspalace/tusky/components/notifications/SeveredRelationshipNotificationViewHolder.kt

@ -16,7 +16,7 @@
package com.keylesspalace.tusky.components.notifications
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.databinding.ItemSeveredRelationshipNotificationBinding
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.NotificationViewData
@ -37,7 +37,7 @@ class SeveredRelationshipNotificationViewHolder(
val event = viewData.event!!
val context = binding.root.context
binding.severedRelationshipText.text = NotificationService.severedRelationShipText(
binding.severedRelationshipText.text = NotificationHelper.severedRelationShipText(
context,
event,
instanceName

10
app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt

@ -18,7 +18,7 @@ package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys
@ -36,7 +36,7 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
lateinit var accountManager: AccountManager
@Inject
lateinit var notificationService: NotificationService
lateinit var notificationHelper: NotificationHelper
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val activeAccount = accountManager.activeAccount ?: return
@ -48,10 +48,10 @@ class NotificationPreferencesFragment : BasePreferencesFragment() {
isChecked = activeAccount.notificationsEnabled
setOnPreferenceChangeListener { _, newValue ->
updateAccount { copy(notificationsEnabled = newValue as Boolean) }
if (notificationService.areNotificationsEnabledBySystem()) {
notificationService.enablePullNotifications()
if (notificationHelper.areNotificationsEnabledBySystem()) {
notificationHelper.enablePullNotifications()
} else {
notificationService.disablePullNotifications()
notificationHelper.disablePullNotifications()
}
true
}

6
app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt

@ -43,7 +43,7 @@ class NotificationFetcher @Inject constructor(
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager,
private val eventHub: EventHub,
private val notificationService: NotificationService,
private val notificationHelper: NotificationHelper,
) {
suspend fun fetchAndShow(accountId: Long?) {
for (account in accountManager.accounts) {
@ -54,14 +54,14 @@ class NotificationFetcher @Inject constructor(
if (account.notificationsEnabled) {
try {
val notifications = fetchNewNotifications(account)
.filter { notificationService.filterNotification(account, it.type) }
.filter { notificationHelper.filterNotification(account, it.type) }
.sortedWith(
compareBy({ it.id.length }, { it.id })
) // oldest notifications first
eventHub.dispatch(NewNotificationsEvent(account.accountId, notifications))
notificationService.show(account, notifications)
notificationHelper.show(account, notifications)
} catch (e: Exception) {
Log.e(TAG, "Error while fetching notifications", e)
}

85
app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationService.kt → app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.kt

@ -46,6 +46,7 @@ import com.keylesspalace.tusky.MainActivity.Companion.openNotificationIntent
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.di.ApplicationScope
@ -56,7 +57,6 @@ import com.keylesspalace.tusky.entity.visibleNotificationTypes
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CryptoUtil
import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.viewdata.buildDescription
@ -73,16 +73,20 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.unifiedpush.android.connector.UnifiedPush
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.ui.SelectDistributorDialogsBuilder
import org.unifiedpush.android.connector.ui.UnifiedPushFunctions
import retrofit2.HttpException
@Singleton
class NotificationService @Inject constructor(
class NotificationHelper @Inject constructor(
private val notificationManager: NotificationManager,
private val accountManager: AccountManager,
private val api: MastodonApi,
private val preferences: SharedPreferences,
@ApplicationContext private val context: Context,
@ApplicationScope private val applicationScope: CoroutineScope,
private val instanceInfoRepository: InstanceInfoRepository
) {
private var workManager: WorkManager = WorkManager.getInstance(context)
@ -648,7 +652,7 @@ class NotificationService @Inject constructor(
.putExtra(KEY_CITED_STATUS_ID, inReplyToId)
.putExtra(KEY_VISIBILITY, replyVisibility)
.putExtra(KEY_SPOILER, contentWarning)
.putExtra(KEY_MENTIONS, mentionedUsernames.toTypedArray<String?>())
.putExtra(KEY_MENTIONS, mentionedUsernames.toTypedArray())
return PendingIntent.getBroadcast(
context.applicationContext,
@ -768,13 +772,36 @@ class NotificationService @Inject constructor(
// make sure this is done in any inconsistent case (is not too often and doesn't hurt).
unregisterPushEndpoint(account)
UnifiedPush.registerAppWithDialog(activity, account.id.toString(), features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE))
// Will lead to call of registerPushEndpoint()
val vapid = instanceInfoRepository.getUpdatedInstanceInfoOrFallback().vapidKey?.replace("=", "")
val builder = SelectDistributorDialogsBuilder(
activity,
object : UnifiedPushFunctions {
override fun getAckDistributor(): String? =
UnifiedPush.getAckDistributor(activity)
override fun getDistributors(): List<String> =
UnifiedPush.getDistributors(activity)
override fun register(instance: String) =
UnifiedPush.register(activity, instance, vapid = vapid)
override fun saveDistributor(distributor: String) =
UnifiedPush.saveDistributor(activity, distributor)
override fun tryUseDefaultDistributor(callback: (Boolean) -> Unit) =
UnifiedPush.tryUseDefaultDistributor(activity, callback)
}
)
builder.instances = listOf(account.id.toString())
builder.mayUseDefault = false
builder.mayUseCurrent = false
builder.run()
}
}
private fun resetPushWhenDistributorIsMissing() {
val lastUsedPushProvider = preferences.getString(PrefKeys.LAST_USED_PUSH_PROVDER, null)
val lastUsedPushProvider = preferences.getString(PrefKeys.LAST_USED_PUSH_PROVIDER, null)
// NOTE UnifiedPush.getSavedDistributor() cannot be used here as that is already null here if the
// distributor was uninstalled.
@ -785,7 +812,7 @@ class NotificationService @Inject constructor(
Log.w(TAG, "Previous push provider ($lastUsedPushProvider) uninstalled. Resetting all accounts.")
preferences.edit {
remove(PrefKeys.LAST_USED_PUSH_PROVDER)
remove(PrefKeys.LAST_USED_PUSH_PROVIDER)
}
applicationScope.launch {
@ -836,7 +863,7 @@ class NotificationService @Inject constructor(
unregisterPushEndpoint(account)
// this probably does nothing (distributor to handle this is missing)
UnifiedPush.unregisterApp(context, account.id.toString())
UnifiedPush.unregister(context, account.id.toString())
}
fun fetchNotificationsOnPushMessage(account: AccountEntity) {
@ -857,26 +884,25 @@ class NotificationService @Inject constructor(
private fun buildAlertSubscriptionData(account: AccountEntity): Map<String, Boolean> =
buildAlertsMap(account).mapKeys { "data[alerts][${it.key}]" }
// Called by UnifiedPush callback in UnifiedPushBroadcastReceiver
// Called by UnifiedPush callback in UnifiedPushService
suspend fun registerPushEndpoint(
account: AccountEntity,
endpoint: String
endpoint: PushEndpoint
) = withContext(Dispatchers.IO) {
// Generate a prime256v1 key pair for WebPush
// Decryption is unimplemented for now, since Mastodon uses an old WebPush
// standard which does not send needed information for decryption in the payload
// This makes it not directly compatible with UnifiedPush
// As of now, we use it purely as a way to trigger a pull
val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1)
val auth = CryptoUtil.secureRandomBytesEncoded(16)
val pubKeySet = endpoint.pubKeySet
if (pubKeySet == null) {
Log.w(TAG, "cannot register push endpoint without public key")
return@withContext
}
api.subscribePushNotifications(
"Bearer ${account.accessToken}",
account.domain,
endpoint,
keyPair.pubkey,
auth,
buildAlertSubscriptionData(account)
auth = "Bearer ${account.accessToken}",
domain = account.domain,
standard = true,
endpoint = endpoint.url,
keysP256DH = pubKeySet.pubKey,
keysAuth = pubKeySet.auth,
data = buildAlertSubscriptionData(account)
).onFailure { throwable ->
Log.w(TAG, "Error setting push endpoint for account ${account.id}", throwable)
disablePushNotificationsForAccount(account)
@ -885,11 +911,12 @@ class NotificationService @Inject constructor(
accountManager.updateAccount(account) {
copy(
pushPubKey = keyPair.pubkey,
pushPrivKey = keyPair.privKey,
pushAuth = auth,
pushPubKey = pubKeySet.pubKey,
// TODO
pushPrivKey = "",
pushAuth = pubKeySet.auth,
pushServerKey = it.serverKey,
unifiedPushUrl = endpoint
unifiedPushUrl = endpoint.url
)
}
@ -897,7 +924,7 @@ class NotificationService @Inject constructor(
Log.d(TAG, "Saving distributor to preferences: $it")
preferences.edit {
putString(PrefKeys.LAST_USED_PUSH_PROVDER, it)
putString(PrefKeys.LAST_USED_PUSH_PROVIDER, it)
}
// TODO once this is selected it cannot be changed (except by wiping the application or uninstalling the provider)
@ -949,7 +976,7 @@ class NotificationService @Inject constructor(
}
companion object {
const val TAG = "NotificationService"
const val TAG = "NotificationHelper"
const val KEY_CITED_STATUS_ID: String = "KEY_CITED_STATUS_ID"
const val KEY_MENTIONS: String = "KEY_MENTIONS"

2
app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java

@ -65,7 +65,7 @@ import java.io.File;
},
// Note: Starting with version 54, database versions in Tusky are always even.
// This is to reserve odd version numbers for use by forks.
version = 70,
version = 72,
autoMigrations = {
@AutoMigration(from = 48, to = 49),
@AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class),

4
app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt

@ -45,7 +45,8 @@ data class InstanceEntity(
val mastodonApiVersion: Int?,
// ToDo: Remove this again when filter v1 support is dropped
@ColumnInfo(defaultValue = "false") val filterV2Supported: Boolean = false
@ColumnInfo(defaultValue = "false") val filterV2Supported: Boolean = false,
val vapidKey: String?
)
@TypeConverters(Converters::class)
@ -72,4 +73,5 @@ data class InstanceInfoEntity(
val maxFieldValueLength: Int?,
val translationEnabled: Boolean?,
val mastodonApiVersion: Int?,
val vapidKey: String?
)

6
app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt

@ -42,6 +42,7 @@ data class Instance(
@JsonClass(generateAdapter = true)
data class Configuration(
val urls: Urls? = null,
val vapid: VapidKey? = null,
val accounts: Accounts? = null,
val statuses: Statuses? = null,
@Json(name = "media_attachments") val mediaAttachments: MediaAttachments? = null,
@ -51,6 +52,11 @@ data class Instance(
@JsonClass(generateAdapter = true)
data class Urls(@Json(name = "streaming_api") val streamingApi: String? = null)
@JsonClass(generateAdapter = true)
data class VapidKey(
@Json(name = "public_key") val publicKey: String? = null
)
@JsonClass(generateAdapter = true)
data class Accounts(
@Json(name = "max_featured_tags") val maxFeaturedTags: Int,

3
app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt

@ -675,7 +675,8 @@ interface MastodonApi {
suspend fun subscribePushNotifications(
@Header("Authorization") auth: String,
@Header(DOMAIN_HEADER) domain: String,
@Field("subscription[endpoint]") endPoint: String,
@Field("subscription[standard]") standard: Boolean,
@Field("subscription[endpoint]") endpoint: String,
@Field("subscription[keys][p256dh]") keysP256DH: String,
@Field("subscription[keys][auth]") keysAuth: String,
// The "data[alerts][]" fields to enable / disable notifications

8
app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt

@ -20,7 +20,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.network.MastodonApi
@ -38,7 +38,7 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
lateinit var accountManager: AccountManager
@Inject
lateinit var notificationService: NotificationService
lateinit var notificationHelper: NotificationHelper
@Inject
@ApplicationScope
@ -46,7 +46,7 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Build.VERSION.SDK_INT < 28) return
if (!notificationService.arePushNotificationsAvailable()) return
if (!notificationHelper.arePushNotificationsAvailable()) return
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -64,7 +64,7 @@ class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
accountManager.getAccountByIdentifier(accountIdentifier)?.let { account ->
if (account.isPushNotificationsEnabled()) {
externalScope.launch {
notificationService.updatePushSubscription(account)
notificationHelper.updatePushSubscription(account)
}
}
}

22
app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt

@ -25,7 +25,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.RemoteInput
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.systemnotifications.NotificationChannelData
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.service.SendStatusService
@ -43,20 +43,20 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == NotificationService.REPLY_ACTION) {
val serverNotificationId = intent.getStringExtra(NotificationService.KEY_SERVER_NOTIFICATION_ID)
val senderId = intent.getLongExtra(NotificationService.KEY_SENDER_ACCOUNT_ID, -1)
if (intent.action == NotificationHelper.REPLY_ACTION) {
val serverNotificationId = intent.getStringExtra(NotificationHelper.KEY_SERVER_NOTIFICATION_ID)
val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1)
val senderIdentifier = intent.getStringExtra(
NotificationService.KEY_SENDER_ACCOUNT_IDENTIFIER
NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER
)!!
val senderFullName = intent.getStringExtra(
NotificationService.KEY_SENDER_ACCOUNT_FULL_NAME
NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME
)
val citedStatusId = intent.getStringExtra(NotificationService.KEY_CITED_STATUS_ID)
val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID)
val visibility =
intent.getSerializableExtraCompat<Status.Visibility>(NotificationService.KEY_VISIBILITY)!!
val spoiler = intent.getStringExtra(NotificationService.KEY_SPOILER).orEmpty()
val mentions = intent.getStringArrayExtra(NotificationService.KEY_MENTIONS).orEmpty()
intent.getSerializableExtraCompat<Status.Visibility>(NotificationHelper.KEY_VISIBILITY)!!
val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER).orEmpty()
val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS).orEmpty()
val account = accountManager.getAccountById(senderId)
@ -137,7 +137,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
private fun getReplyMessage(intent: Intent): CharSequence {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
return remoteInput?.getCharSequence(NotificationService.KEY_REPLY, "") ?: ""
return remoteInput?.getCharSequence(NotificationHelper.KEY_REPLY, "") ?: ""
}
companion object {

45
app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt → app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushService.kt

@ -15,60 +15,57 @@
package com.keylesspalace.tusky.receiver
import android.content.Context
import android.util.Log
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.network.MastodonApi
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.MessagingReceiver
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.PushService
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
@AndroidEntryPoint
class UnifiedPushBroadcastReceiver : MessagingReceiver() {
class UnifiedPushService : PushService() {
@Inject
lateinit var accountManager: AccountManager
@Inject
lateinit var mastodonApi: MastodonApi
@Inject
lateinit var notificationService: NotificationService
lateinit var notificationHelper: NotificationHelper
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
override fun onMessage(context: Context, message: ByteArray, instance: String) {
Log.d(TAG, "New message received for account $instance: #${message.size}")
val account = accountManager.getAccountById(instance.toLong())
account?.let {
notificationService.fetchNotificationsOnPushMessage(it)
override fun onMessage(message: PushMessage, instance: String) {
Log.d(TAG, "New message received for account $instance: $message")
accountManager.getAccountById(instance.toLong())?.let { account ->
notificationHelper.fetchNotificationsOnPushMessage(account)
}
}
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
Log.d(TAG, "Endpoint available for account $instance: $endpoint")
accountManager.getAccountById(instance.toLong())?.let {
applicationScope.launch { notificationService.registerPushEndpoint(it, endpoint) }
accountManager.getAccountById(instance.toLong())?.let { account ->
applicationScope.launch { notificationHelper.registerPushEndpoint(account, endpoint) }
}
}
override fun onRegistrationFailed(context: Context, instance: String) = Unit
override fun onRegistrationFailed(reason: FailedReason, instance: String) = Unit
override fun onUnregistered(context: Context, instance: String) {
override fun onUnregistered(instance: String) {
Log.d(TAG, "Endpoint unregistered for account $instance")
accountManager.getAccountById(instance.toLong())?.let {
// It's fine if the account does not exist anymore -- that means it has been logged out
// TODO its not: this is the Mastodon side and should be done (unregistered)
applicationScope.launch { notificationService.unregisterPushEndpoint(it) }
accountManager.getAccountById(instance.toLong())?.let { account ->
// It's fine if the account does not exist anymore -- that means it has been logged out,
// which removes the subscription anyway
applicationScope.launch { notificationHelper.unregisterPushEndpoint(account) }
}
}
companion object {
const val TAG = "UnifiedPushBroadcastReceiver"
const val TAG = "UnifiedPushService"
}
}

2
app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt

@ -55,7 +55,7 @@ object PrefKeys {
// each preference a key for it to work.
const val SCHEMA_VERSION: String = "schema_version"
const val LAST_USED_PUSH_PROVDER = "lastUsedPushProvider"
const val LAST_USED_PUSH_PROVIDER = "lastUsedPushProvider"
const val APP_THEME = "appTheme"
const val LANGUAGE = "language"

6
app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt

@ -1,7 +1,7 @@
package com.keylesspalace.tusky.usecase
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.DatabaseCleaner
import com.keylesspalace.tusky.db.entity.AccountEntity
@ -15,7 +15,7 @@ class LogoutUsecase @Inject constructor(
private val accountManager: AccountManager,
private val draftHelper: DraftHelper,
private val shareShortcutHelper: ShareShortcutHelper,
private val notificationService: NotificationService,
private val notificationHelper: NotificationHelper,
) {
/**
@ -35,7 +35,7 @@ class LogoutUsecase @Inject constructor(
)
}
notificationService.disableNotificationsForAccount(account)
notificationHelper.disableNotificationsForAccount(account)
// remove account from local AccountManager
val otherAccountAvailable = accountManager.remove(account) != null

60
app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt

@ -1,60 +0,0 @@
/* Copyright 2022 Tusky contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util
import android.util.Base64
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.Security
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.interfaces.ECPrivateKey
import org.bouncycastle.jce.interfaces.ECPublicKey
import org.bouncycastle.jce.provider.BouncyCastleProvider
object CryptoUtil {
const val CURVE_PRIME256_V1 = "prime256v1"
private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
private fun secureRandomBytes(len: Int): ByteArray {
val ret = ByteArray(len)
SecureRandom.getInstance("SHA1PRNG").nextBytes(ret)
return ret
}
fun secureRandomBytesEncoded(len: Int): String {
return Base64.encodeToString(secureRandomBytes(len), BASE64_FLAGS)
}
data class EncodedKeyPair(val pubkey: String, val privKey: String)
fun generateECKeyPair(curve: String): EncodedKeyPair {
val spec = ECNamedCurveTable.getParameterSpec(curve)
val gen = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
gen.initialize(spec)
val keyPair = gen.genKeyPair()
val pubKey = keyPair.public as ECPublicKey
val privKey = keyPair.private as ECPrivateKey
val encodedPubKey = Base64.encodeToString(pubKey.q.getEncoded(false), BASE64_FLAGS)
val encodedPrivKey = Base64.encodeToString(privKey.d.toByteArray(), BASE64_FLAGS)
return EncodedKeyPair(encodedPubKey, encodedPrivKey)
}
}

8
app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt

@ -25,7 +25,7 @@ import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.systemnotifications.NotificationFetcher
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -34,9 +34,9 @@ class NotificationWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher,
notificationService: NotificationService,
notificationHelper: NotificationHelper,
) : CoroutineWorker(appContext, params) {
val notification: Notification = notificationService.createWorkerNotification(
val notification: Notification = notificationHelper.createWorkerNotification(
R.string.notification_notification_worker
)
@ -47,7 +47,7 @@ class NotificationWorker @AssistedInject constructor(
}
override suspend fun getForegroundInfo() = ForegroundInfo(
NotificationService.NOTIFICATION_ID_FETCH_NOTIFICATION,
NotificationHelper.NOTIFICATION_ID_FETCH_NOTIFICATION,
notification
)

8
app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt

@ -25,7 +25,7 @@ import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.DatabaseCleaner
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
@ -39,9 +39,9 @@ class PruneCacheWorker @AssistedInject constructor(
@Assisted workerParams: WorkerParameters,
private val databaseCleaner: DatabaseCleaner,
private val accountManager: AccountManager,
val notificationService: NotificationService,
val notificationHelper: NotificationHelper,
) : CoroutineWorker(appContext, workerParams) {
val notification: Notification = notificationService.createWorkerNotification(
val notification: Notification = notificationHelper.createWorkerNotification(
R.string.notification_prune_cache
)
@ -57,7 +57,7 @@ class PruneCacheWorker @AssistedInject constructor(
}
override suspend fun getForegroundInfo() = ForegroundInfo(
NotificationService.NOTIFICATION_ID_PRUNE_CACHE,
NotificationHelper.NOTIFICATION_ID_PRUNE_CACHE,
notification
)

10
app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt

@ -13,7 +13,7 @@ import androidx.work.testing.WorkManagerTestInitHelper
import at.connyduck.calladapter.networkresult.NetworkResult
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
import com.keylesspalace.tusky.components.systemnotifications.NotificationService
import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.entity.Account
@ -99,7 +99,7 @@ class MainActivityTest {
val notificationManager = context.getSystemService(NotificationManager::class.java)
val shadowNotificationManager = shadowOf(notificationManager)
val notificationService = NotificationService(
val notificationHelper = NotificationHelper(
notificationManager,
mock {
on { areNotificationsEnabled() } doReturn true
@ -110,10 +110,10 @@ class MainActivityTest {
mock(),
)
notificationService.createNotificationChannelsForAccount(accountEntity)
notificationHelper.createNotificationChannelsForAccount(accountEntity)
runInBackground {
val notification = notificationService.createBaseNotification(
val notification = notificationHelper.createBaseNotification(
Notification(
type = type,
id = "id",
@ -167,7 +167,7 @@ class MainActivityTest {
eventHub = eventHub,
accountManager = accountManager,
shareShortcutHelper = mock(),
notificationService = mock(),
notificationHelper = mock(),
)
val testViewModelFactory = viewModelFactory {
initializer { viewModel }

7
gradle/libs.versions.toml

@ -47,7 +47,8 @@ robolectric = "4.14.1"
sparkbutton = "4.2.0"
touchimageview = "3.7.1"
turbine = "1.2.0"
unified-push = "2.4.0"
unified-push-connector = "3.0.9"
unified-push-connector-ui = "1.1.0"
xmlwriter = "1.0.4"
[plugins]
@ -127,7 +128,8 @@ robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectr
sparkbutton = { module = "at.connyduck.sparkbutton:sparkbutton", version.ref = "sparkbutton" }
touchimageview = { module = "com.github.MikeOrtiz:TouchImageView", version.ref = "touchimageview" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
unified-push = { module = "com.github.UnifiedPush:android-connector", version.ref = "unified-push" }
unified-push-connector = { module = "org.unifiedpush.android:connector", version.ref = "unified-push-connector" }
unified-push-connector-ui = { module = "org.unifiedpush.android:connector-ui", version.ref = "unified-push-connector-ui" }
xmlwriter = { module = "org.pageseeder.xmlwriter:pso-xmlwriter", version.ref = "xmlwriter" }
[bundles]
@ -146,3 +148,4 @@ okhttp = ["okhttp-core", "okhttp-logging-interceptor"]
retrofit = ["retrofit-core", "retrofit-converter-moshi"]
room = ["androidx-room-ktx", "androidx-room-paging"]
xmldiff = ["diffx", "xmlwriter"]
unifiedpush = ["unified-push-connector", "unified-push-connector-ui"]

83
gradle/verification-metadata.xml

@ -6387,12 +6387,12 @@
<artifact name="aapt2-8.9.2-12782657-linux.jar">
<sha256 value="ffb7b7d419c7341dd013f2f87bfb0148f8bff0a2a47c222f54935b3b8f233605" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-8.9.2-12782657-osx.jar">
<sha256 value="5d0aec6851fffbc9f6c8a8b50390c0bc976aa0ddf57a8a3a39061ed54b609ad1" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-8.9.2-12782657-windows.jar">
<sha256 value="a1c812cc3100ef5ae90d46c12b2b8288133691ab63c5ec7b0f9a5da23b26549e" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-8.9.2-12782657-osx.jar">
<sha256 value="5d0aec6851fffbc9f6c8a8b50390c0bc976aa0ddf57a8a3a39061ed54b609ad1" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-8.9.2-12782657-windows.jar">
<sha256 value="a1c812cc3100ef5ae90d46c12b2b8288133691ab63c5ec7b0f9a5da23b26549e" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-8.9.2-12782657.pom">
<sha256 value="ea462f7f0d66a2bce906d761f51e391aeaea30bf790d8c4fe2082c747ef3be0b" origin="Generated by Gradle"/>
</artifact>
@ -13068,6 +13068,22 @@
<sha256 value="af781c9a5766ffea311a0df0536576a64decc661aa110c4de5c73ac8bf434424" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.crypto.tink" name="tink" version="1.16.0">
<artifact name="tink-1.16.0.jar">
<sha256 value="7f5e380dde874cd44613952f374723da1f8636526c6a574945bceb0e65571d0a" origin="Generated by Gradle"/>
</artifact>
<artifact name="tink-1.16.0.pom">
<sha256 value="28393661ae65329dcf782ffd7978b0a1ba0ad614577ee4b0bbb7bebdc4319673" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.crypto.tink" name="tink" version="1.17.0">
<artifact name="tink-1.17.0.jar">
<sha256 value="1f5e1f52e1295b4c35d2257c3cd14d715f81872e928f5bfb1c39cfb89585411d" origin="Generated by Gradle"/>
</artifact>
<artifact name="tink-1.17.0.pom">
<sha256 value="452b543a98e9339e57996fb6148233e323beab5643dce6a36561a44979ba1135" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.crypto.tink" name="tink" version="1.7.0">
<artifact name="tink-1.7.0.jar">
<sha256 value="88970a456a08ba4c66b01b23e5846ca1095cc14e54cb48363e5d2e15a1307308" origin="Generated by Gradle"/>
@ -14505,6 +14521,14 @@
<sha256 value="920135797dcca5917b5a5c017642a58d340a4cd1bcd12f56f892a5663bd7bddc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.22.0">
<artifact name="error_prone_annotations-2.22.0.jar">
<sha256 value="82a027b86541f58d1f9ee020cdf6bebe82acc7a267d3c53a2ea5cd6335932bbd" origin="Generated by Gradle"/>
</artifact>
<artifact name="error_prone_annotations-2.22.0.pom">
<sha256 value="b725c521514168e9c20d3719a76aa01b50e5a7282859f4ea84c251cf9f842080" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.23.0">
<artifact name="error_prone_annotations-2.23.0.jar">
<sha256 value="ec6f39f068b6ff9ac323c68e28b9299f8c0a80ca512dccb1d4a70f40ac3ec054" origin="Generated by Gradle"/>
@ -14573,6 +14597,11 @@
<sha256 value="82f889260fe1d50ff6300f3b1977a7a80cd794f95f1b48ef210b85e4d16109cd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.22.0">
<artifact name="error_prone_parent-2.22.0.pom">
<sha256 value="5d2522bea83df5a581ccd6b227635ca34e0d397b7672de748aca4157080573c7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.23.0">
<artifact name="error_prone_parent-2.23.0.pom">
<sha256 value="f5470a4b3104fe309fbe94a80d16c3c54d20f748f4c5de4f68a428688f30cbd4" origin="Generated by Gradle"/>
@ -14850,6 +14879,11 @@
<sha256 value="04ecfd52c50df07a75551f9b09e0ef30630ee4237d091ca0eca71e5bfb78cce5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-bom" version="4.28.2">
<artifact name="protobuf-bom-4.28.2.pom">
<sha256 value="43666cd072728c646cc45a614aeeead1e0b23f818f652982df279a0e5adaf316" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-java" version="3.22.3">
<artifact name="protobuf-java-3.22.3.jar">
<sha256 value="59d388ea6a2d2d76ae8efff7fd4d0c60c6f0f464c3d3ab9be8e5add092975708" origin="Generated by Gradle"/>
@ -14866,6 +14900,14 @@
<sha256 value="3941221ca657819ddebd95fe8b740f470af7abf30460b13ea1c9ab79f10fab91" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-java" version="4.28.2">
<artifact name="protobuf-java-4.28.2.jar">
<sha256 value="707bccf406f4fc61b841d4700daa8d3e84db8ab499ef3481a060fa6a0f06e627" origin="Generated by Gradle"/>
</artifact>
<artifact name="protobuf-java-4.28.2.pom">
<sha256 value="1eaecce23fb47b1ae54996f16c0d5485563a972ec262b78a8180e07f0aa77253" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-java-util" version="3.22.3">
<artifact name="protobuf-java-util-3.22.3.jar">
<sha256 value="c615f76879dc5c303e4df5b94a6afa39534058c7545db2d483fd95d9f63c8bfe" origin="Generated by Gradle"/>
@ -14900,6 +14942,11 @@
<sha256 value="fb7ec0505876fdb9ec79510aced2d1eb04c3b8cfc690b589049757ca981cadb3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.protobuf" name="protobuf-parent" version="4.28.2">
<artifact name="protobuf-parent-4.28.2.pom">
<sha256 value="f7f23fc16ed463cf52579fffafa03940e8e653b33c6b4f106082c5bf15050f75" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.testing.platform" name="android-device-provider-local" version="0.0.9-alpha02">
<artifact name="android-device-provider-local-0.0.9-alpha02.jar">
<sha256 value="446d0fca4e3711e3e37219cf888ec12b4dec3f14999ee439735385fb787e914b" origin="Generated by Gradle"/>
@ -20354,6 +20401,30 @@
<sha256 value="0fe31326e83b7622cbcd9c75d467b291f01ffe3cc6e3e76651ac05a1c1c7a360" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.unifiedpush.android" name="connector" version="3.0.7">
<artifact name="connector-3.0.7.aar">
<sha256 value="84b8edca39e4393985848acc1aa7530c6703b2851aae25d007cc7f45e20659c0" origin="Generated by Gradle"/>
</artifact>
<artifact name="connector-3.0.7.module">
<sha256 value="2149687d8bc7d0a78295c0e03ba0157c6c58ce1f3804c16a1a2713f4fbbd7847" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.unifiedpush.android" name="connector" version="3.0.9">
<artifact name="connector-3.0.9.aar">
<sha256 value="ab25d22aa4822244fefec8e2011902711341fccb70af4a50f708e77c98477cea" origin="Generated by Gradle"/>
</artifact>
<artifact name="connector-3.0.9.module">
<sha256 value="e3380bda701ebd089b709ec0be9206a74ae63cfcc0f7127019d57f55e07f474a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.unifiedpush.android" name="connector-ui" version="1.1.0">
<artifact name="connector-ui-1.1.0.aar">
<sha256 value="154c8346344277a9ecd49ea7821a400f621a32f1099c94ca961c6eb8b611b093" origin="Generated by Gradle"/>
</artifact>
<artifact name="connector-ui-1.1.0.module">
<sha256 value="bec6e094adc821624f8f52a7b8df0612c7d1423a7a838ec691b981160f8c8a7b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.xerial" name="sqlite-jdbc" version="3.41.2.2">
<artifact name="sqlite-jdbc-3.41.2.2.jar">
<sha256 value="0cdab410947e04b6743df99cf1543267ddd107357d6f76948d145be590fd497d" origin="Generated by Gradle"/>

Loading…
Cancel
Save