Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dee39e7
chore(deps): Restrict jitpack content
p1gp1g Dec 9, 2025
ac79fee
Add UnifiedPush lib
p1gp1g Jan 14, 2026
1934b5f
Add webpush capability
p1gp1g Jan 14, 2026
965b6b7
Add webpush requests
p1gp1g Jan 14, 2026
e6ef9dc
Add UnifiedPush switch in settings
p1gp1g Jan 14, 2026
2c8a649
Show notif permissions for UnifiedPush too
p1gp1g Jan 14, 2026
862b9fe
Add UnifiedPush to diagnose activity
p1gp1g Jan 14, 2026
de7b1ef
Register for push notifications to UnifiedPush and server
p1gp1g Jan 14, 2026
e31c6f3
Fix settings activity
p1gp1g Jan 14, 2026
0ad2291
Fix PushRegistrationWorker
p1gp1g Jan 15, 2026
c3a3dac
Fix API return type for webpush
p1gp1g Jan 15, 2026
bec4549
Fix web push jobs
p1gp1g Jan 15, 2026
1c277cf
Add instanceFor function to centralized generation of UP instances fo…
p1gp1g Jan 15, 2026
b2c702f
Add UnifiedPushService, register new endpoint and activate web push
p1gp1g Jan 15, 2026
bbcc85c
Unregister from web push when using proxyPush
p1gp1g Jan 15, 2026
94ee433
Process push notifications with UnifiedPush
p1gp1g Jan 15, 2026
d7cc257
Allow user to select non-default distributor
p1gp1g Jan 15, 2026
5ce1038
Fix endpoint registration
p1gp1g Jan 15, 2026
681a31b
Fix proxy push unregistration
p1gp1g Jan 16, 2026
bf225a2
Log error correctly
p1gp1g Jan 16, 2026
bf71b06
Fix proxy push with multiple account
p1gp1g Jan 16, 2026
367a6ab
Handle post-push registration in a single place
p1gp1g Jan 16, 2026
335ec81
Use `when` to handle event status
p1gp1g Jan 16, 2026
caec07b
Check once if the event is core internalAccountId
p1gp1g Jan 16, 2026
0c8c16f
Handle post-profile storage with the eventbus
p1gp1g Jan 16, 2026
d6f0ac3
Fetch capabilities before registering for Push notifications
p1gp1g Jan 16, 2026
ef9b459
Register with UnifiedPush when needed during AccountVerification
p1gp1g Jan 16, 2026
5045f29
Periodically register for UnifiedPush
p1gp1g Jan 16, 2026
a0b408a
Fix disable UnifiedPush when adding new UP account without web push
p1gp1g Jan 16, 2026
2456da7
Fix push notification registration for new account without webpush wh…
p1gp1g Jan 17, 2026
ad58e9a
Fix useUnifiedPush with first user verification
p1gp1g Jan 17, 2026
ce81301
Do not show UnifiedPush Service settings when UP isn't shown
p1gp1g Jan 17, 2026
75113e1
Request notif permission with UnifiedPush
p1gp1g Jan 17, 2026
37123a5
Show latest endpoint reception in diagnose
p1gp1g Jan 17, 2026
571dddb
Change log for registration failure
p1gp1g Jan 17, 2026
62f4998
Unregister web push from distrib
p1gp1g Jan 17, 2026
78ad018
Show notif when UnifiedPush distrib is removed
p1gp1g Jan 17, 2026
0f1d119
Add comment to explain why we disable UnifiedPush
p1gp1g Jan 17, 2026
6ba5c1b
feat(unifiedpush): May show an introduction dialog if the user has mu…
p1gp1g Feb 17, 2026
657f59c
Fix add comment for notif on unregister
p1gp1g Feb 17, 2026
d3b56cb
feat(unifiedpush): Unregister from Distributor when disabling UP
p1gp1g Feb 17, 2026
2a01a65
feat(unifiedpush): Add user.id to logs during web push registration
p1gp1g Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ configurations.configureEach {
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly
String protobufJava = "com.google.protobuf:protobuf-java:4.28.2"
resolutionStrategy {
force(protobufJava)
dependencySubstitution {
substitute module("com.google.protobuf:protobuf-javalite") using module(protobufJava)
}
}
}

dependencies {
Expand Down Expand Up @@ -261,6 +268,9 @@ dependencies {
implementation 'com.github.lukaspili.autodagger2:autodagger2:1.1'
kapt 'com.github.lukaspili.autodagger2:autodagger2-compiler:1.1'
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
// Until https://github.com/google/dagger/pull/5062
kapt 'org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0'


implementation 'org.greenrobot:eventbus:3.3.1'
implementation 'net.zetetic:sqlcipher-android:4.13.0'
Expand Down Expand Up @@ -320,6 +330,8 @@ dependencies {
gplayImplementation 'com.google.android.gms:play-services-base:18.10.0'
gplayImplementation "com.google.firebase:firebase-messaging:25.0.1"

implementation 'org.unifiedpush.android:connector:3.3.1'

//compose
implementation(platform("androidx.compose:compose-bom:2026.02.00"))
implementation("androidx.compose.ui:ui")
Expand Down Expand Up @@ -422,4 +434,4 @@ detekt {

ksp {
arg('room.schemaLocation', "$projectDir/schemas")
}
}
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@
android:exported="false"
android:foregroundServiceType="microphone|camera" />

<service android:name=".services.UnifiedPushService"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
</intent-filter>
</service>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ import com.nextcloud.talk.jobs.WebsocketConnectionsWorker
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.generic.Status
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.ui.dialog.IntroduceUnifiedPushDialog
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.UnifiedPushUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
Expand All @@ -58,6 +60,7 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.unifiedpush.android.connector.UnifiedPush
import java.net.CookieManager
import javax.inject.Inject

Expand Down Expand Up @@ -260,18 +263,7 @@ class AccountVerificationActivity : BaseActivity() {
@SuppressLint("SetTextI18n")
override fun onSuccess(user: User) {
internalAccountId = user.id!!
if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
ClosedInterfaceImpl().setUpPushTokenRegistration()
} else {
Log.w(TAG, "Skipping push registration.")
runOnUiThread {
binding.progressText.text =
""" ${binding.progressText.text}
${resources!!.getString(R.string.nc_push_disabled)}
""".trimIndent()
}
fetchAndStoreCapabilities()
}
eventBus.post(EventStatus(user.id!!, EventStatus.EventType.PROFILE_STORED, true))
}

@SuppressLint("SetTextI18n")
Expand Down Expand Up @@ -346,41 +338,128 @@ class AccountVerificationActivity : BaseActivity() {
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(eventStatus: EventStatus) {
Log.d(TAG, "caught EventStatus of type " + eventStatus.eventType.toString())
if (eventStatus.eventType == EventStatus.EventType.PUSH_REGISTRATION) {
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
runOnUiThread {
binding.progressText.text =
"""
if (internalAccountId != eventStatus.userId) {
Log.d(TAG, "Event isn't for us. Aborting.")
return
}
// We do: PROFILE_STORED
// -> CAPABILITIES_FETCH
// -> PUSH_REGISTRATION
// -> SIGNALING_SETTINGS
when (eventStatus.eventType) {
EventStatus.EventType.PROFILE_STORED -> {
fetchAndStoreCapabilities()
}
EventStatus.EventType.CAPABILITIES_FETCH -> {
if (!eventStatus.isAllGood) {
runOnUiThread {
binding.progressText.text =
"""
${binding.progressText.text}
${resources!!.getString(R.string.nc_push_disabled)}
${resources!!.getString(R.string.nc_capabilities_failed)}
""".trimIndent()
}
abortVerification()
} else {
setupPushNotifications()
}
}
fetchAndStoreCapabilities()
} else if (eventStatus.eventType == EventStatus.EventType.CAPABILITIES_FETCH) {
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
runOnUiThread {
binding.progressText.text =
"""
EventStatus.EventType.PUSH_REGISTRATION -> {
if (!eventStatus.isAllGood) {
runOnUiThread {
binding.progressText.text =
"""
${binding.progressText.text}
${resources!!.getString(R.string.nc_capabilities_failed)}
${resources!!.getString(R.string.nc_push_disabled)}
""".trimIndent()
}
}
abortVerification()
} else if (internalAccountId == eventStatus.userId && eventStatus.isAllGood) {
fetchAndStoreExternalSignalingSettings()
}
} else if (eventStatus.eventType == EventStatus.EventType.SIGNALING_SETTINGS) {
if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
runOnUiThread {
binding.progressText.text =
"""
EventStatus.EventType.SIGNALING_SETTINGS -> {
if (!eventStatus.isAllGood) {
runOnUiThread {
binding.progressText.text =
"""
${binding.progressText.text}
${resources!!.getString(R.string.nc_external_server_failed)}
""".trimIndent()
}
}
proceedWithLogin()
}
else -> {}
}
}

private fun setupPushNotifications() {
// This isn't a first account, and UnifiedPush is enabled.
if (appPreferences.useUnifiedPush) {
if (userManager.getUserWithId(internalAccountId).blockingGet().hasWebPushCapability) {
UnifiedPushUtils.registerWithCurrentDistributor(context)
eventBus.post(EventStatus(internalAccountId, EventStatus.EventType.PUSH_REGISTRATION, true))
return
} else {
Log.w(TAG, "Warning: disabling UnifiedPush, user server doesn't support web push.")
appPreferences.useUnifiedPush = false
}
}

// - By default, use the Play Services if available
// - If this is a first user, and we have an External UnifiedPush distributor,
// and the server supports it: we use it
// - Else we skip push registrations
if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
ClosedInterfaceImpl().setUpPushTokenRegistration()
eventBus.post(EventStatus(internalAccountId, EventStatus.EventType.PUSH_REGISTRATION, true))
} else if (userManager.users.blockingGet().size == 1 &&
UnifiedPush.getDistributors(context).isNotEmpty() &&
userManager.getUserWithId(internalAccountId).blockingGet().hasWebPushCapability) {
useUnifiedPushIntroduced()
} else {
Log.w(TAG, "Skipping push registration.")
eventBus.post(EventStatus(internalAccountId, EventStatus.EventType.PUSH_REGISTRATION, false))
}
}

/**
* Show a dialog if the user has to select their distributor
*
* Most of the time, nothing will be shown, as most users have
* a single distributor, or already selected their default one
*/
private fun useUnifiedPushIntroduced() {
if (UnifiedPushUtils.usingDefaultDistributorNeedsIntro(context)) {
dialogForUnifiedPush { res ->
if (res) {
useUnifiedPush()
}
}
} else {
useUnifiedPush()
}
}

private fun useUnifiedPush() {
UnifiedPushUtils.useDefaultDistributor(this) { distrib ->
distrib?.let {
Log.d(TAG, "UnifiedPush registered with $distrib")
appPreferences.useUnifiedPush = true
eventBus.post(EventStatus(internalAccountId, EventStatus.EventType.PUSH_REGISTRATION, true))
} ?: run {
Log.d(TAG, "No UnifiedPush distrib selected")
eventBus.post(EventStatus(internalAccountId, EventStatus.EventType.PUSH_REGISTRATION, false))
}
}
}

private fun dialogForUnifiedPush(onResponse: (Boolean) -> Unit) {
binding.genericComposeView.apply {
setContent {
IntroduceUnifiedPushDialog { res ->
onResponse(res)
}
}
proceedWithLogin()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.UnifiedPushUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import io.reactivex.Observer
Expand Down Expand Up @@ -260,7 +261,11 @@ class MainActivity :

override fun onSuccess(users: List<User>) {
if (users.isNotEmpty()) {
ClosedInterfaceImpl().setUpPushTokenRegistration()
if (appPreferences.useUnifiedPush) {
UnifiedPushUtils.setPeriodicPushRegistrationWorker(this@MainActivity)
} else {
ClosedInterfaceImpl().setUpPushTokenRegistration()
}
runOnUiThread {
openConversationList()
}
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/com/nextcloud/talk/api/NcApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.models.json.participants.TalkBanOverall;
import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
import com.nextcloud.talk.models.json.push.VapidOverall;
import com.nextcloud.talk.models.json.reactions.ReactionsOverall;
import com.nextcloud.talk.models.json.reminder.ReminderOverall;
import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
Expand Down Expand Up @@ -270,6 +271,32 @@ Observable<GenericOverall> setUserData(@Header("Authorization") String authoriza
@GET
Observable<Status> getServerStatus(@Url String url);

@GET
Observable<VapidOverall> getVapidKey(
@Header("Authorization") String authorization,
@Url String url);

@FormUrlEncoded
@POST
Observable<Response<GenericOverall>> registerWebPush(
@Header("Authorization") String authorization,
@Url String url,
@Field("endpoint") String endpoint,
@Field("uaPublicKey") String uaPublicKey,
@Field("auth") String auth,
@Field("appTypes") String appTypes);

@FormUrlEncoded
@POST
Observable<Response<GenericOverall>> activateWebPush(
@Header("Authorization") String authorization,
@Url String url,
@Field("activationToken") String activationToken);

@DELETE
Observable<GenericOverall> unregisterWebPush(
@Header("Authorization") String authorization,
@Url String url);

/*
QueryMap items are as follows:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ class ConversationsListActivity :
// handle notification permission on API level >= 33
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!platformPermissionUtil.isPostNotificationsPermissionGranted() &&
ClosedInterfaceImpl().isGooglePlayServicesAvailable
(ClosedInterfaceImpl().isGooglePlayServicesAvailable ||
appPreferences.useUnifiedPush)
) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/com/nextcloud/talk/data/user/model/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ data class User(
var scheduledForDeletion: Boolean = FALSE
) : Parcelable {

val hasWebPushCapability: Boolean
get() = capabilities?.notificationsCapability?.push?.contains("webpush") == true

fun getCredentials(): String = ApiUtils.getCredentials(username, token)!!

fun hasSpreedFeatureCapability(capabilityName: String): Boolean {
Expand Down
Loading