Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3374962
chore(deps): Restrict jitpack content
p1gp1g Dec 4, 2025
c70655d
chore: Fix capability update
p1gp1g Dec 4, 2025
a4dc493
chore(notif): Support delete-multiple use dedicated function to handl…
p1gp1g Dec 4, 2025
02e0c65
feat(unifiedpush): Add UnifiedPush service
p1gp1g Dec 4, 2025
126f9bb
feat(unifiedpush): Add settings to enable UnifiedPush
p1gp1g Dec 4, 2025
9bed2fe
feat(unifiedpush): Register web push and proxy push on startup
p1gp1g Dec 4, 2025
eb2a814
feat(unifiedpush): Unregister UnifiedPush/web push when needed
p1gp1g Dec 4, 2025
9c5e10a
feat(unifiedpush): Do not register for talk notif if installed
p1gp1g Dec 5, 2025
e02820b
feat(unifiedpush): Allow selecting another distrib
p1gp1g Dec 5, 2025
2d6c8ed
feat(unifiedpush): Show UnifiedPush settings only if a service is ava…
p1gp1g Dec 5, 2025
482f035
feat(webpush): Fix appTypes name
p1gp1g Feb 13, 2026
622b06d
feat(unifiedpush): Clarify object and function names
p1gp1g Feb 13, 2026
244be55
feat(unifiedpush): Fix preference when dismissed
p1gp1g Feb 16, 2026
51f7d57
feat(unifiedpush): Show introduction dialog if the user needs to pick…
p1gp1g Feb 16, 2026
49d293e
feat(unifiedpush): Enable UnifiedPush by default for generic flavor
p1gp1g Feb 16, 2026
447a785
feat(unifiedpush): Use worker to process UnifiedPush events
p1gp1g Feb 16, 2026
116688e
feat(unifiedpush): Fix dialog by running in the UI thread
p1gp1g Feb 16, 2026
8489d3d
feat(unifiedpush): Show notification to ask user to open the app on u…
p1gp1g Feb 16, 2026
68e6013
feat(unifiedpush): Fix missing nextcloud talk pkg name
p1gp1g Feb 17, 2026
8ac264a
Update gradle-verification
p1gp1g Jul 2, 2026
03662fa
Lint
p1gp1g Jul 3, 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 @@ -147,30 +147,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 @@ -512,6 +517,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 @@ -654,6 +654,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 @@ -45,6 +45,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.activity.BaseActivity;
import com.owncloud.android.ui.activity.ConflictsResolveActivity;
Expand Down Expand Up @@ -344,6 +345,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 @@ -94,6 +94,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 @@ -223,6 +224,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 @@ -130,6 +130,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,
Expand Down
Loading