Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ tasks.named("check").configure {
}

dependencies {
// unified push
implementation(libs.unifiedpush.connector)
implementation(libs.unifiedpush.connector.ui)

// region Nextcloud library
implementation(libs.android.library) {
exclude(group = "org.ogce", module = "xpp3") // unused in Android and brings wrong Junit version
Expand Down
21 changes: 21 additions & 0 deletions app/src/generic/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
~ SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

<application>
<service android:name="com.nextcloud.unifiedpush.UnifiedPush"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
</intent-filter>
</service>
</application>
</manifest>
132 changes: 132 additions & 0 deletions app/src/generic/java/com/nextcloud/unifiedpush/UnifiedPush.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.unifiedpush

import android.content.Context
import android.os.PowerManager
import androidx.work.WorkManager
import com.google.gson.Gson
import com.nextcloud.client.core.ClockImpl
import com.nextcloud.client.jobs.BackgroundJobManagerImpl
import com.nextcloud.client.jobs.NotificationWork
import com.nextcloud.client.preferences.AppPreferencesImpl
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
import com.owncloud.android.datamodel.PushConfigurationState
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.activity.DrawerActivity
import com.owncloud.android.utils.PushUtils
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.PushService
import org.unifiedpush.android.connector.UnifiedPush
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
import org.unifiedpush.android.connector.ui.SelectDistributorDialogsBuilder
import org.unifiedpush.android.connector.ui.UnifiedPushFunctions

class UnifiedPush : PushService() {
companion object {
private val TAG: String? = UnifiedPush::class.java.simpleName
private const val WAKELOCK_TIMEOUT = 5000L

fun registerForPushMessaging(activity: DrawerActivity?, accountName: String, forceChoose: Boolean) {
if ((activity === null) || (activity.mHandler === null) || activity.isFinishing)
return

// run on ui thread
activity.mHandler.post {
SelectDistributorDialogsBuilder(
activity,
object : UnifiedPushFunctions {
override fun tryUseDefaultDistributor(callback: (Boolean) -> Unit) =
UnifiedPush.tryUseDefaultDistributor(activity, callback).also {
Log_OC.d(TAG, "tryUseDefaultDistributor()")
}

override fun getAckDistributor(): String? =
UnifiedPush.getAckDistributor(activity).also {
Log_OC.d(TAG, "getAckDistributor() = $it")
}

override fun getDistributors(): List<String> =
UnifiedPush.getDistributors(activity).also {
Log_OC.d(TAG, "getDistributors() = $it")
}

override fun register(instance: String) =
UnifiedPush.register(activity, instance).also {
Log_OC.d(TAG, "register($instance)")
}

override fun saveDistributor(distributor: String) =
UnifiedPush.saveDistributor(activity, distributor).also {
Log_OC.d(TAG, "saveDistributor($distributor)")
}
}
).apply {
instances = listOf(accountName)
mayUseCurrent = !forceChoose
mayUseDefault = !forceChoose
}.run()
}
}

fun unregisterForPushMessaging(context: Context, accountName: String) {
// unregister with distributor
UnifiedPush.unregister(context, accountName)

// delete locally saved endpoint value
ArbitraryDataProviderImpl(context).deleteKeyForAccount(accountName, PushUtils.KEY_PUSH)
}
}

override fun onMessage(message: PushMessage, instance: String) {
// get a wake lock to 'help' background job run more promptly since it can take minutes to run if phone is
// sleeping/dozing - 5 secs should be well long enough to get the notification displayed
val wakeLock = (getSystemService(POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "nc:timedPartialwakelock")
wakeLock?.acquire(WAKELOCK_TIMEOUT)

// called when a new message is received. The message contains the full POST body of the push message
Log_OC.d(TAG, "unified push message received")

BackgroundJobManagerImpl(
WorkManager.getInstance(this), ClockImpl(), AppPreferencesImpl.fromContext(this)
).startNotificationJob(
message.content.toString(Charsets.UTF_8),
instance,
NotificationWork.BACKEND_TYPE_UNIFIED_PUSH
)
}

override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
Log_OC.d(TAG, "onNewEndpoint(${endpoint.url}, $instance)")

val newAccountPushData = PushConfigurationState()
newAccountPushData.setPushToken(endpoint.url)
ArbitraryDataProviderImpl(this).storeOrUpdateKeyValue(
instance,
PushUtils.KEY_PUSH,
Gson().toJson(newAccountPushData)
)
}

override fun onRegistrationFailed(reason: FailedReason, instance: String) =
// the registration is not possible, eg. no network
// force unregister to make sure cleaned up. re-register will be re-attempted next time
UnifiedPush.unregister(this, instance).also {
Log_OC.d(TAG, "onRegistrationFailed(${reason.name}, $instance)")
}

override fun onUnregistered(instance: String) =
// this application is unregistered by the distributor from receiving push messages
// force unregister to make sure cleaned up. re-register will be re-attempted next time
UnifiedPush.unregister(this, instance).also {
Log_OC.d(TAG, "onUnregistered($instance)")
}

}
37 changes: 31 additions & 6 deletions app/src/generic/java/com/owncloud/android/utils/PushUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@

import android.content.Context;

import android.accounts.Account;
import android.text.TextUtils;

import com.google.gson.Gson;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.preferences.AppPreferencesImpl;
import com.owncloud.android.MainApp;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.PushConfigurationState;
import com.owncloud.android.datamodel.SignatureVerification;
import com.nextcloud.unifiedpush.UnifiedPush;
import com.owncloud.android.ui.activity.DrawerActivity;

import java.security.Key;

Expand All @@ -22,13 +30,31 @@ public final class PushUtils {
private PushUtils() {
}

public static void pushRegistrationToServer(final UserAccountManager accountManager, final String pushToken) {
// do nothing
public static void updateRegistrationsWithServer(
final DrawerActivity activity,
final UserAccountManager accountManager,
final String pushToken) {
for (Account account : accountManager.getAccounts()) {
String providerValue = new ArbitraryDataProviderImpl(MainApp.getAppContext()).getValue(account.name, KEY_PUSH);
PushConfigurationState accountPushData = new Gson().fromJson(providerValue, PushConfigurationState.class);

if ((accountPushData != null) && accountPushData.isShouldBeDeleted()) {
// unregister push notifications
UnifiedPush.Companion.unregisterForPushMessaging(MainApp.getAppContext(), account.name);
} else {
// else, (re-)register for push notifications
if (activity != null) {
UnifiedPush.Companion.registerForPushMessaging(
activity,
account.name,
((accountPushData == null) || TextUtils.isEmpty(accountPushData.getPushToken())));
}
}
}
}

public static void reinitKeys(UserAccountManager accountManager) {
Context context = MainApp.getAppContext();
AppPreferencesImpl.fromContext(context).setKeysReInitEnabled();
AppPreferencesImpl.fromContext(MainApp.getAppContext()).setKeysReInitEnabled();
}

public static Key readKeyFromFile(boolean readPublicKey) {
Expand All @@ -39,8 +65,7 @@ public static SignatureVerification verifySignature(
final Context context,
final UserAccountManager accountManager,
final byte[] signatureBytes,
final byte[] subjectBytes
) {
final byte[] subjectBytes) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
final String subject = data.get(NotificationWork.KEY_NOTIFICATION_SUBJECT);
final String signature = data.get(NotificationWork.KEY_NOTIFICATION_SIGNATURE);
if (subject != null && signature != null) {
backgroundJobManager.startNotificationJob(subject, signature);
backgroundJobManager.startNotificationJob(subject,
signature,
NotificationWork.BACKEND_TYPE_FIREBASE_CLOUD_MESSAGING);
}
}

Expand All @@ -97,7 +99,7 @@ public void onNewToken(@NonNull String newToken) {

if (!TextUtils.isEmpty(getResources().getString(R.string.push_server_url))) {
preferences.setPushToken(newToken);
PushUtils.pushRegistrationToServer(accountManager, preferences.getPushToken());
PushUtils.updateRegistrationsWithServer(null, accountManager, preferences.getPushToken());
}
}
}
8 changes: 6 additions & 2 deletions app/src/gplay/java/com/owncloud/android/utils/PushUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.owncloud.android.lib.resources.notifications.UnregisterAccountDeviceForNotificationsOperation;
import com.owncloud.android.lib.resources.notifications.UnregisterAccountDeviceForProxyOperation;
import com.owncloud.android.lib.resources.notifications.models.PushResponse;
import com.owncloud.android.ui.activity.DrawerActivity;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.FileUtils;
Expand Down Expand Up @@ -169,7 +170,10 @@ private static void deleteRegistrationForAccount(Account account) {
}
}

public static void pushRegistrationToServer(final UserAccountManager accountManager, final String token) {
public static void updateRegistrationsWithServer(
final Context unusedContext,
final UserAccountManager accountManager,
final String token) {
arbitraryDataProvider = new ArbitraryDataProviderImpl(MainApp.getAppContext());

if (!TextUtils.isEmpty(MainApp.getAppContext().getResources().getString(R.string.push_server_url)) &&
Expand Down Expand Up @@ -352,7 +356,7 @@ public static void reinitKeys(final UserAccountManager accountManager) {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.nextcloud.client.preferences.AppPreferencesImpl;
import com.owncloud.android.MainApp;
import com.owncloud.android.datamodel.SignatureVerification;
import com.owncloud.android.ui.activity.DrawerActivity;

import java.security.Key;

Expand All @@ -22,7 +23,10 @@ public final class PushUtils {
private PushUtils() {
}

public static void pushRegistrationToServer(final UserAccountManager accountManager, final String pushToken) {
public static void updateRegistrationsWithServer(
final Context context,
final UserAccountManager accountManager,
final String pushToken) {
// do nothing
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ class AccountRemovalWork(
PushUtils.KEY_PUSH,
gson.toJson(pushArbitraryData)
)
PushUtils.pushRegistrationToServer(userAccountManager, pushArbitraryData.getPushToken())
PushUtils.updateRegistrationsWithServer(
null, // can pass null for context because this is guaranteed to be an 'unregister' operation
userAccountManager,
pushArbitraryData.getPushToken())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ interface BackgroundJobManager {
fun scheduleMediaFoldersDetectionJob()
fun startMediaFoldersDetectionJob()

fun startNotificationJob(subject: String, signature: String)
fun startNotificationJob(subject: String, signature: String, backendType: Int)
fun startAccountRemovalJob(accountName: String, remoteWipe: Boolean)
fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean)
fun getFileUploads(user: User): LiveData<List<JobInfo>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,10 +611,11 @@ internal class BackgroundJobManagerImpl(
)
}

override fun startNotificationJob(subject: String, signature: String) {
override fun startNotificationJob(subject: String, signature: String, backendType: Int) {
val data = Data.Builder()
.putString(NotificationWork.KEY_NOTIFICATION_SUBJECT, subject)
.putString(NotificationWork.KEY_NOTIFICATION_SIGNATURE, signature)
.putInt(NotificationWork.KEY_NOTIFICATION_TYPE, backendType)
.build()

val request = oneTimeRequestBuilder(NotificationWork::class, JOB_NOTIFICATION)
Expand Down
Loading