Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0a003a4
chore(deps): Restrict jitpack content
p1gp1g Dec 4, 2025
022fb02
chore: Fix capability update
p1gp1g Dec 4, 2025
31f1d8e
chore(notif): Support delete-multiple use dedicated function to handl…
p1gp1g Dec 4, 2025
7dc6143
feat(unifiedpush): Add UnifiedPush service
p1gp1g Dec 4, 2025
e8948ef
feat(unifiedpush): Add settings to enable UnifiedPush
p1gp1g Dec 4, 2025
65ecbce
feat(unifiedpush): Register web push and proxy push on startup
p1gp1g Dec 4, 2025
b3266d2
feat(unifiedpush): Unregister UnifiedPush/web push when needed
p1gp1g Dec 4, 2025
42b1426
feat(unifiedpush): Do not register for talk notif if installed
p1gp1g Dec 5, 2025
45b746f
feat(unifiedpush): Allow selecting another distrib
p1gp1g Dec 5, 2025
a5a7b19
feat(unifiedpush): Show UnifiedPush settings only if a service is ava…
p1gp1g Dec 5, 2025
d75b3b4
feat(webpush): Fix appTypes name
p1gp1g Feb 13, 2026
9266be0
feat(unifiedpush): Clarify object and function names
p1gp1g Feb 13, 2026
db60c6b
feat(unifiedpush): Fix preference when dismissed
p1gp1g Feb 16, 2026
080c8c3
feat(unifiedpush): Show introduction dialog if the user needs to pick…
p1gp1g Feb 16, 2026
7254db8
feat(unifiedpush): Enable UnifiedPush by default for generic flavor
p1gp1g Feb 16, 2026
984ac4b
feat(unifiedpush): Use worker to process UnifiedPush events
p1gp1g Feb 16, 2026
5e5dc66
feat(unifiedpush): Fix dialog by running in the UI thread
p1gp1g Feb 16, 2026
545b881
feat(unifiedpush): Show notification to ask user to open the app on u…
p1gp1g Feb 16, 2026
84bf56d
feat(unifiedpush): Fix missing nextcloud talk pkg name
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
9 changes: 9 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -139,30 +139,35 @@ android {
register("generic") {
applicationId = "com.nextcloud.client"
dimension = "default"
buildConfigField("boolean", "DEFAULT_PUSH_UNIFIEDPUSH", "true")
}

register("gplay") {
applicationId = "com.nextcloud.client"
dimension = "default"
buildConfigField("boolean", "DEFAULT_PUSH_UNIFIEDPUSH", "false")
}

register("huawei") {
applicationId = "com.nextcloud.client"
dimension = "default"
buildConfigField("boolean", "DEFAULT_PUSH_UNIFIEDPUSH", "false")
}

register("versionDev") {
applicationId = "com.nextcloud.android.beta"
dimension = "default"
versionCode = 20220322
versionName = "20220322"
buildConfigField("boolean", "DEFAULT_PUSH_UNIFIEDPUSH", "false")
}

register("qa") {
applicationId = "com.nextcloud.android.qa"
dimension = "default"
versionCode = 1
versionName = "1"
buildConfigField("boolean", "DEFAULT_PUSH_UNIFIEDPUSH", "false")
}
}
}
Expand Down Expand Up @@ -500,6 +505,10 @@ dependencies {
"gplayImplementation"(libs.bundles.gplay)
// endregion

// region Push
implementation(libs.unifiedpush.connector)
// endregion

// region common
implementation(libs.ui)
implementation(libs.common.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
package com.owncloud.android.utils;

import android.accounts.Account;
import android.content.Context;

import com.nextcloud.client.account.UserAccountManager;
Expand All @@ -22,6 +23,10 @@ public final class PushUtils {
private PushUtils() {
}

public static void setRegistrationForAccountEnabled(Account account, Boolean enabled) {
// do nothing
}

public static void pushRegistrationToServer(final UserAccountManager accountManager, final String pushToken) {
// do nothing
}
Expand Down
64 changes: 58 additions & 6 deletions app/src/gplay/java/com/owncloud/android/utils/PushUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,24 @@ private static int generateRsa2048KeyPair() {
return -2;
}

private static void deleteRegistrationForAccount(Account account) {
/**
* Tag the registration as disabled in the local data provider
*
* @param account
*/
public static void setRegistrationForAccountEnabled(Account account, Boolean enabled) {
String arbitraryValue;
if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account.name, KEY_PUSH))) {
Gson gson = new Gson();
PushConfigurationState pushArbitraryData = gson.fromJson(arbitraryValue,
PushConfigurationState.class);
pushArbitraryData.shouldBeDisabled = !enabled;
if (enabled) pushArbitraryData.disabled = false;
arbitraryDataProvider.storeOrUpdateKeyValue(account.name, KEY_PUSH, gson.toJson(pushArbitraryData));
}
}

private static void deleteRegistrationForAccount(Account account, Boolean deleteLocalData) {
Context context = MainApp.getAppContext();
OwnCloudAccount ocAccount;
arbitraryDataProvider = new ArbitraryDataProviderImpl(MainApp.getAppContext());
Expand All @@ -141,7 +158,8 @@ private static void deleteRegistrationForAccount(Account account) {
RemoteOperationResult<Void> remoteOperationResult =
new UnregisterAccountDeviceForNotificationsOperation().execute(mClient);

if (remoteOperationResult.getHttpCode() == HttpStatus.SC_ACCEPTED) {
int status = remoteOperationResult.getHttpCode();
if (deleteLocalData && status == HttpStatus.SC_ACCEPTED) {
String arbitraryValue;
if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account.name, KEY_PUSH))) {
Gson gson = new Gson();
Expand All @@ -157,6 +175,19 @@ private static void deleteRegistrationForAccount(Account account) {
arbitraryDataProvider.deleteKeyForAccount(account.name, KEY_PUSH);
}
}
} else if (!deleteLocalData && (status == HttpStatus.SC_ACCEPTED || status == HttpStatus.SC_OK)) {
String arbitraryValue;
if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account.name, KEY_PUSH))) {
Gson gson = new Gson();
PushConfigurationState pushArbitraryData = gson.fromJson(arbitraryValue,
PushConfigurationState.class);
pushArbitraryData.disabled = true;
pushArbitraryData.pushToken = "";
arbitraryDataProvider.storeOrUpdateKeyValue(
account.name,
KEY_PUSH,
gson.toJson(pushArbitraryData));
}
}
} catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
Log_OC.d(TAG, "Failed to find an account");
Expand Down Expand Up @@ -197,9 +228,19 @@ public static void pushRegistrationToServer(final UserAccountManager accountMana
accountPushData = null;
}

if (accountPushData == null || providerValue.isEmpty()) {
Log_OC.d(TAG, "accountPushData is null");
} else {
Log_OC.d(TAG, "ShouldBeDeleted=" + accountPushData.isShouldBeDeleted() +
" disabled=" + accountPushData.disabled +
" shouldBeDisabled=" + accountPushData.shouldBeDisabled
+ " sameToken=" + accountPushData.getPushToken().equals(token));
}
if (accountPushData != null && !accountPushData.getPushToken().equals(token) &&
!accountPushData.isShouldBeDeleted() ||
!accountPushData.isShouldBeDeleted() && !accountPushData.disabled &&
!accountPushData.shouldBeDisabled ||
TextUtils.isEmpty(providerValue)) {
Log_OC.d(TAG, "Registering " + account.name);
try {
OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
NextcloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton().
Expand Down Expand Up @@ -244,7 +285,11 @@ public static void pushRegistrationToServer(final UserAccountManager accountMana
Log_OC.d(TAG, "Failed via OperationCanceledException");
}
} else if (accountPushData != null && accountPushData.isShouldBeDeleted()) {
deleteRegistrationForAccount(account);
Log_OC.d(TAG, "Deleting " + account.name);
deleteRegistrationForAccount(account, true);
} else if (accountPushData != null && accountPushData.shouldBeDisabled && !accountPushData.disabled) {
Log_OC.d(TAG, "Disabling " + account.name);
deleteRegistrationForAccount(account, false);
}
}
}
Expand Down Expand Up @@ -336,11 +381,19 @@ private static int saveKeyToFile(Key key, String path) {
return -1;
}

/**
* Reinit keys, [pushRegistrationToServer]\* must be called to take effect
*
* \* You likely need to call [UnifiedPushUtils.registerCurrentPushConfiguration], which will call
* pushRegistrationToServer if needed
*
* @param accountManager
*/
public static void reinitKeys(final UserAccountManager accountManager) {
Context context = MainApp.getAppContext();
Account[] accounts = accountManager.getAccounts();
for (Account account : accounts) {
deleteRegistrationForAccount(account);
deleteRegistrationForAccount(account, true);
}

String keyPath = context.getDir("nc-keypair", Context.MODE_PRIVATE).getAbsolutePath();
Expand All @@ -352,7 +405,6 @@ public static void reinitKeys(final UserAccountManager accountManager) {

AppPreferences preferences = AppPreferencesImpl.fromContext(context);
String pushToken = preferences.getPushToken();
pushRegistrationToServer(accountManager, pushToken);
preferences.setKeysReInitEnabled();
}

Expand Down
5 changes: 5 additions & 0 deletions app/src/huawei/java/com/owncloud/android/utils/PushUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
package com.owncloud.android.utils;

import android.accounts.Account;
import android.content.Context;

import com.nextcloud.client.account.UserAccountManager;
Expand All @@ -22,6 +23,10 @@ public final class PushUtils {
private PushUtils() {
}

public static void setRegistrationForAccountEnabled(Account account, Boolean enabled) {
// do nothing
}

public static void pushRegistrationToServer(final UserAccountManager accountManager, final String pushToken) {
// do nothing
}
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,14 @@
android:enabled="true"
android:exported="true"
tools:ignore="ExportedService" />
<service
android:name="com.owncloud.android.services.UnifiedPushService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
</intent-filter>
</service>

<activity
android:name=".ui.activity.SsoGrantPermissionActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import com.owncloud.android.providers.UsersAndGroupsSearchProvider;
import com.owncloud.android.services.AccountManagerService;
import com.owncloud.android.services.OperationsService;
import com.owncloud.android.services.UnifiedPushService;
import com.owncloud.android.syncadapter.FileSyncService;
import com.owncloud.android.ui.activities.ActivitiesActivity;
import com.owncloud.android.ui.activity.BaseActivity;
Expand Down Expand Up @@ -349,6 +350,9 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract OperationsService operationsService();

@ContributesAndroidInjector
abstract UnifiedPushService unifiedPushService();

@ContributesAndroidInjector
abstract PlayerService playerService();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.owncloud.android.ui.activity.ManageAccountsActivity
import com.owncloud.android.ui.events.AccountRemovedEvent
import com.owncloud.android.utils.EncryptionUtils
import com.owncloud.android.utils.PushUtils
import com.owncloud.android.utils.CommonPushUtils
import org.greenrobot.eventbus.EventBus
import java.util.Optional

Expand Down Expand Up @@ -94,6 +95,7 @@ class AccountRemovalWork(
)
// unregister push notifications
unregisterPushNotifications(context, user, arbitraryDataProvider)
unregisterWebPushNotifications(context, user)

// remove pending account removal
arbitraryDataProvider.deleteKeyForAccount(user.accountName, ManageAccountsActivity.PENDING_FOR_REMOVAL)
Expand Down Expand Up @@ -170,6 +172,13 @@ class AccountRemovalWork(
}
}

private fun unregisterWebPushNotifications(
context: Context,
user: User
) {
CommonPushUtils.unregisterUnifiedPushForAccount(context, userAccountManager, user.toOwnCloudAccount())
}

private fun removeSyncedFolders(context: Context, user: User, clock: Clock) {
val syncedFolders = syncedFolderProvider.syncedFolders
val syncedFolderIds: MutableList<Long> = ArrayList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class BackgroundJobFactory @Inject constructor(
OfflineSyncWork::class -> createOfflineSyncWork(context, workerParameters)
MediaFoldersDetectionWork::class -> createMediaFoldersDetectionWork(context, workerParameters)
NotificationWork::class -> createNotificationWork(context, workerParameters)
UnifiedPushWork::class -> createUnifiedPushWork(context, workerParameters)
AccountRemovalWork::class -> createAccountRemovalWork(context, workerParameters)
CalendarBackupWork::class -> createCalendarBackupWork(context, workerParameters)
CalendarImportWork::class -> createCalendarImportWork(context, workerParameters)
Expand Down Expand Up @@ -215,6 +216,14 @@ class BackgroundJobFactory @Inject constructor(
viewThemeUtils.get()
)

private fun createUnifiedPushWork(context: Context, params: WorkerParameters): UnifiedPushWork = UnifiedPushWork(
context,
params,
accountManager,
preferences,
viewThemeUtils.get()
)

private fun createAccountRemovalWork(context: Context, params: WorkerParameters): AccountRemovalWork =
AccountRemovalWork(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ interface BackgroundJobManager {
fun startMediaFoldersDetectionJob()

fun startNotificationJob(subject: String, signature: String)
fun startDecryptedNotificationJob(accountName: String, message: String)
fun registerWebPush(accountName: String, url: String, uaPublicKey: String, auth: String)
fun activateWebPush(accountName: String, token: String)
fun unregisterWebPush(accountName: String)
fun mayResetUnifiedPush()

fun startAccountRemovalJob(accountName: String, remoteWipe: Boolean)
fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean)
fun getFileUploads(user: User): LiveData<List<JobInfo>>
Expand Down
Loading