Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9fa046b
chore(deps): Restrict jitpack content
p1gp1g Dec 9, 2025
5224a6c
Add UnifiedPush lib
p1gp1g Jan 14, 2026
165e8d4
Add webpush capability
p1gp1g Jan 14, 2026
0dd20c3
Add webpush requests
p1gp1g Jan 14, 2026
1f35fb7
Add UnifiedPush switch in settings
p1gp1g Jan 14, 2026
bb10526
Show notif permissions for UnifiedPush too
p1gp1g Jan 14, 2026
f87f830
Add UnifiedPush to diagnose activity
p1gp1g Jan 14, 2026
ad8b6bc
Register for push notifications to UnifiedPush and server
p1gp1g Jan 14, 2026
6f83476
Fix settings activity
p1gp1g Jan 14, 2026
9ac712e
Fix PushRegistrationWorker
p1gp1g Jan 15, 2026
b42184d
Fix API return type for webpush
p1gp1g Jan 15, 2026
678b81e
Fix web push jobs
p1gp1g Jan 15, 2026
430631c
Add instanceFor function to centralized generation of UP instances fo…
p1gp1g Jan 15, 2026
0e321ef
Add UnifiedPushService, register new endpoint and activate web push
p1gp1g Jan 15, 2026
f46767b
Unregister from web push when using proxyPush
p1gp1g Jan 15, 2026
3bd66ec
Process push notifications with UnifiedPush
p1gp1g Jan 15, 2026
33f5a7b
Allow user to select non-default distributor
p1gp1g Jan 15, 2026
9c38392
Fix endpoint registration
p1gp1g Jan 15, 2026
15bc6e0
Fix proxy push unregistration
p1gp1g Jan 16, 2026
29100b4
Log error correctly
p1gp1g Jan 16, 2026
a3550c6
Fix proxy push with multiple account
p1gp1g Jan 16, 2026
0b150e2
Handle post-push registration in a single place
p1gp1g Jan 16, 2026
1dbd636
Use `when` to handle event status
p1gp1g Jan 16, 2026
89f48ab
Check once if the event is core internalAccountId
p1gp1g Jan 16, 2026
a5f812b
Handle post-profile storage with the eventbus
p1gp1g Jan 16, 2026
c13a1bb
Fetch capabilities before registering for Push notifications
p1gp1g Jan 16, 2026
b8449ee
Register with UnifiedPush when needed during AccountVerification
p1gp1g Jan 16, 2026
ece2043
Periodically register for UnifiedPush
p1gp1g Jan 16, 2026
8d0ef2e
Fix disable UnifiedPush when adding new UP account without web push
p1gp1g Jan 16, 2026
d535342
Fix push notification registration for new account without webpush wh…
p1gp1g Jan 17, 2026
0e121d7
Fix useUnifiedPush with first user verification
p1gp1g Jan 17, 2026
2327fe8
Do not show UnifiedPush Service settings when UP isn't shown
p1gp1g Jan 17, 2026
d08af61
Request notif permission with UnifiedPush
p1gp1g Jan 17, 2026
befa400
Show latest endpoint reception in diagnose
p1gp1g Jan 17, 2026
8a32271
Change log for registration failure
p1gp1g Jan 17, 2026
1467638
Unregister web push from distrib
p1gp1g Jan 17, 2026
b2aaf90
Show notif when UnifiedPush distrib is removed
p1gp1g Jan 17, 2026
9b725a4
Add comment to explain why we disable UnifiedPush
p1gp1g Jan 17, 2026
5cc0658
feat(unifiedpush): May show an introduction dialog if the user has mu…
p1gp1g Feb 17, 2026
e9bc028
Fix add comment for notif on unregister
p1gp1g Feb 17, 2026
2e9d6e4
feat(unifiedpush): Unregister from Distributor when disabling UP
p1gp1g Feb 17, 2026
8fe657c
feat(unifiedpush): Add user.id to logs during web push registration
p1gp1g Feb 17, 2026
e8f3c3b
Fix baseUrl after rebase
p1gp1g Apr 13, 2026
1cf7a5e
Fix after rebase: Diagnosis string name
p1gp1g Apr 13, 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
10 changes: 10 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ 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
val 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 @@ -322,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.2")

// compose
implementation(platform("androidx.compose:compose-bom:2026.03.01"))
implementation("androidx.compose.ui:ui")
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,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 @@ -304,7 +304,8 @@ class ConversationsListActivity : BaseActivity() {
// 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