Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
144df1d
chore(deps): Restrict jitpack content
p1gp1g Dec 9, 2025
4ba7e20
Add UnifiedPush lib
p1gp1g Jan 14, 2026
4cefe7e
Add webpush capability
p1gp1g Jan 14, 2026
97c8d04
Add webpush requests
p1gp1g Jan 14, 2026
b173465
Add UnifiedPush switch in settings
p1gp1g Jan 14, 2026
2ab5f82
Show notif permissions for UnifiedPush too
p1gp1g Jan 14, 2026
31c661e
Add UnifiedPush to diagnose activity
p1gp1g Jan 14, 2026
1f9ed53
Register for push notifications to UnifiedPush and server
p1gp1g Jan 14, 2026
9b1d483
Fix settings activity
p1gp1g Jan 14, 2026
24a0280
Fix PushRegistrationWorker
p1gp1g Jan 15, 2026
81ced85
Fix API return type for webpush
p1gp1g Jan 15, 2026
b9a4a55
Fix web push jobs
p1gp1g Jan 15, 2026
4c05acd
Add instanceFor function to centralized generation of UP instances fo…
p1gp1g Jan 15, 2026
7af055f
Add UnifiedPushService, register new endpoint and activate web push
p1gp1g Jan 15, 2026
fadb0f9
Unregister from web push when using proxyPush
p1gp1g Jan 15, 2026
3c19a4f
Process push notifications with UnifiedPush
p1gp1g Jan 15, 2026
166cf5a
Allow user to select non-default distributor
p1gp1g Jan 15, 2026
1c81933
Fix endpoint registration
p1gp1g Jan 15, 2026
c79168e
Fix proxy push unregistration
p1gp1g Jan 16, 2026
9525b3b
Log error correctly
p1gp1g Jan 16, 2026
e2d5326
Fix proxy push with multiple account
p1gp1g Jan 16, 2026
610fd9b
Handle post-push registration in a single place
p1gp1g Jan 16, 2026
f53fdb5
Use `when` to handle event status
p1gp1g Jan 16, 2026
fd79961
Check once if the event is core internalAccountId
p1gp1g Jan 16, 2026
21cb342
Handle post-profile storage with the eventbus
p1gp1g Jan 16, 2026
d691d6b
Fetch capabilities before registering for Push notifications
p1gp1g Jan 16, 2026
8cb5e53
Register with UnifiedPush when needed during AccountVerification
p1gp1g Jan 16, 2026
6e09f43
Periodically register for UnifiedPush
p1gp1g Jan 16, 2026
af52d27
Fix disable UnifiedPush when adding new UP account without web push
p1gp1g Jan 16, 2026
60d7374
Fix push notification registration for new account without webpush wh…
p1gp1g Jan 17, 2026
d358122
Fix useUnifiedPush with first user verification
p1gp1g Jan 17, 2026
6cf439f
Do not show UnifiedPush Service settings when UP isn't shown
p1gp1g Jan 17, 2026
9d657b3
Request notif permission with UnifiedPush
p1gp1g Jan 17, 2026
7345e59
Show latest endpoint reception in diagnose
p1gp1g Jan 17, 2026
a000537
Change log for registration failure
p1gp1g Jan 17, 2026
dc50259
Unregister web push from distrib
p1gp1g Jan 17, 2026
4a1807b
Show notif when UnifiedPush distrib is removed
p1gp1g Jan 17, 2026
3bdd26b
Add comment to explain why we disable UnifiedPush
p1gp1g Jan 17, 2026
7734633
feat(unifiedpush): May show an introduction dialog if the user has mu…
p1gp1g Feb 17, 2026
afc61e7
Fix add comment for notif on unregister
p1gp1g Feb 17, 2026
5748ee9
feat(unifiedpush): Unregister from Distributor when disabling UP
p1gp1g Feb 17, 2026
7c6a1e3
feat(unifiedpush): Add user.id to logs during web push registration
p1gp1g Feb 17, 2026
1c51b3f
Fix baseUrl after rebase
p1gp1g Apr 13, 2026
3d75dc3
Fix after rebase: Diagnosis string name
p1gp1g Apr 13, 2026
a885736
Add comment for the protobuf-java dependency resolution
p1gp1g Apr 30, 2026
cf44ea1
format code + cleanup
mahibi Apr 30, 2026
5eabd20
fix user for notifications
mahibi Apr 30, 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
17 changes: 17 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,21 @@ 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

// com.google.crypto.tink (pulled in by org.unifiedpush.android:connector) transitively depends on
// protobuf-java, which conflicts with protobuf-javalite used by com.google.mediapipe:tasks-core,
// pulled by com.google.mediapipe:tasks-vision
//
// To analyse the dependencies:
// `./gradlew :app:dependencyInsight --configuration genericDebugRuntimeClasspath --dependency com.google.protobuf:protobuf-java`
val protobufJava = "com.google.protobuf:protobuf-java:4.28.2"
resolutionStrategy {
force(protobufJava)
dependencySubstitution {
substitute(module("com.google.protobuf:protobuf-javalite"))
.using(module(protobufJava))
}
}
Comment thread
p1gp1g marked this conversation as resolved.
}

dependencies {
Expand Down Expand Up @@ -323,6 +338,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.04.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 @@ -336,6 +336,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,129 @@ 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)}
""".trimIndent()
${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)}
""".trimIndent()
${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()
""".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 @@ -305,7 +305,10 @@ 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
Loading