Merge pull request #589 from p1gp1g/unifiedpush/push_settings

Unifiedpush/push settings
This commit is contained in:
LucasGGamerM 2025-03-11 08:54:31 -03:00 committed by GitHub
commit f4ffd4718e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 152 additions and 58 deletions

View file

@ -164,7 +164,7 @@ dependencies {
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0' implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
annotationProcessor 'org.parceler:parceler:1.1.12' annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'com.github.UnifiedPush:android-connector:2.1.1' implementation 'org.unifiedpush.android:connector:3.0.2'
androidTestImplementation 'androidx.test:core:1.5.0' androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'

View file

@ -6,6 +6,7 @@ import android.content.Context;
import android.webkit.WebView; import android.webkit.WebView;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.utils.UnifiedPushHelper;
import me.grishka.appkit.imageloader.ImageCache; import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.NetworkUtils; import me.grishka.appkit.utils.NetworkUtils;
@ -27,7 +28,11 @@ public class MastodonApp extends Application{
ImageCache.setParams(params); ImageCache.setParams(params);
NetworkUtils.setUserAgent("MoshidonAndroid/"+BuildConfig.VERSION_NAME); NetworkUtils.setUserAgent("MoshidonAndroid/"+BuildConfig.VERSION_NAME);
if (UnifiedPushHelper.isUnifiedPushEnabled(this)){
UnifiedPushHelper.registerAllAccounts(this);
} else {
PushSubscriptionManager.tryRegisterFCM(); PushSubscriptionManager.tryRegisterFCM();
}
GlobalUserPreferences.load(); GlobalUserPreferences.load();
if(BuildConfig.DEBUG){ if(BuildConfig.DEBUG){
WebView.setWebContentsDebuggingEnabled(true); WebView.setWebContentsDebuggingEnabled(true);

View file

@ -163,7 +163,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, account, notification), account.getID(), notification); PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, account, notification), account.getID(), notification);
} }
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){ void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
NotificationManager nm=context.getSystemService(NotificationManager.class); NotificationManager nm=context.getSystemService(NotificationManager.class);
AccountSession session=AccountSessionManager.get(accountID); AccountSession session=AccountSessionManager.get(accountID);
Account self=session.self; Account self=session.self;

View file

@ -5,14 +5,22 @@ import android.util.Log;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.notifications.GetNotificationByID;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.PushNotification;
import org.unifiedpush.android.connector.FailedReason;
import org.unifiedpush.android.connector.MessagingReceiver; import org.unifiedpush.android.connector.MessagingReceiver;
import org.unifiedpush.android.connector.data.PublicKeySet;
import org.unifiedpush.android.connector.data.PushEndpoint;
import org.unifiedpush.android.connector.data.PushMessage;
import java.util.List; import java.util.List;
import java.util.function.Function;
import kotlin.text.Charsets;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
@ -24,16 +32,23 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
} }
@Override @Override
public void onNewEndpoint(@NotNull Context context, @NotNull String endpoint, @NotNull String instance) { public void onNewEndpoint(@NotNull Context context, @NotNull PushEndpoint endpoint, @NotNull String instance) {
// Called when a new endpoint be used for sending push messages // Called when a new endpoint be used for sending push messages
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint + " for "+ instance); Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint.getUrl() + " for "+ instance);
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance); AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
if (account != null) if (account != null) {
account.getPushSubscriptionManager().registerAccountForPush(null, endpoint); PublicKeySet ks = endpoint.getPubKeySet();
if (ks != null){
account.getPushSubscriptionManager().registerAccountForPush(account.pushSubscription, true, endpoint.getUrl(), ks.getPubKey(), ks.getAuth());
} else {
// ks should never be null on new endpoint
account.getPushSubscriptionManager().registerAccountForPush(account.pushSubscription, endpoint.getUrl());
}
}
} }
@Override @Override
public void onRegistrationFailed(@NotNull Context context, @NotNull String instance) { public void onRegistrationFailed(@NotNull Context context, @NotNull FailedReason reason, @NotNull String instance) {
// called when the registration is not possible, eg. no network // called when the registration is not possible, eg. no network
Log.d(TAG, "onRegistrationFailed: " + instance); Log.d(TAG, "onRegistrationFailed: " + instance);
//re-register for gcm //re-register for gcm
@ -53,26 +68,46 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
} }
@Override @Override
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String instance) { public void onMessage(@NotNull Context context, @NotNull PushMessage message, @NotNull String instance) {
Log.d(TAG, "New message for " + instance);
// Called when a new message is received. The message contains the full POST body of the push message // Called when a new message is received. The message contains the full POST body of the push message
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance); AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
if (account == null) if (account == null)
return; return;
//this is stupid if (message.getDecrypted()) {
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush, // If the mastodon server supports the standard webpush, we can directly use the content
// thus it is not possible to decrypt them. SO we need to re-request them from the server and transform them later on Log.d(TAG, "Push message correctly decrypted");
// The official uses fcm and moves the headers to extra data, see PushNotification pn = MastodonAPIController.gson.fromJson(new String(message.getContent(), Charsets.UTF_8), PushNotification.class);
// https://github.com/mastodon/webpush-fcm-relay/blob/cac95b28d5364b0204f629283141ac3fb749e0c5/webpush-fcm-relay.go#L116 new GetNotificationByID(pn.notificationId)
// https://github.com/tuskyapp/Tusky/pull/2303#issue-1112080540 .setCallback(new Callback<>(){
@Override
public void onSuccess(org.joinmastodon.android.model.Notification result){
MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notify(context, pn, instance, result));
}
@Override
public void onError(ErrorResponse error){
MastodonAPIController.runInBackground(()-> new PushNotificationReceiver().notify(context, pn, instance, null));
}
})
.exec(instance);
} else {
// else, we have to sync with the server
Log.d(TAG, "Server doesn't support standard webpush, fetching one notification");
fetchOneNotification(context, account, (notif) -> () -> new PushNotificationReceiver().notifyUnifiedPush(context, account, notif));
}
}
private void fetchOneNotification(@NotNull Context context, @NotNull AccountSession account, @NotNull Function<Notification, Runnable> callback) {
account.getCacheController().getNotifications(null, 1, false, false, true, new Callback<>(){ account.getCacheController().getNotifications(null, 1, false, false, true, new Callback<>(){
@Override @Override
public void onSuccess(PaginatedResponse<List<Notification>> result){ public void onSuccess(PaginatedResponse<List<Notification>> result){
result.items result.items
.stream() .stream()
.findFirst() .findFirst()
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, account, value))); .ifPresent(value->MastodonAPIController.runInBackground(callback.apply(value)));
} }
@Override @Override

View file

@ -166,12 +166,23 @@ public class PushSubscriptionManager{
//work-around for adding the randomAccountId //work-around for adding the randomAccountId
String newEndpoint = endpoint; String newEndpoint = endpoint;
if (endpoint.startsWith("https://app.joinmastodon.org/relay-to/fcm/")) Boolean standard = true;
if (endpoint.startsWith("https://app.joinmastodon.org/relay-to/fcm/")){
newEndpoint+=pushAccountID; newEndpoint+=pushAccountID;
standard = false;
}
new RegisterForPushNotifications(newEndpoint, registerAccountForPush(subscription, standard, newEndpoint, encodedPublicKey, encodedAuthKey);
encodedPublicKey, });
encodedAuthKey, }
public void registerAccountForPush(PushSubscription subscription, Boolean standard, String endpoint, String p256dh, String auth){
MastodonAPIController.runInBackground(()->{
Log.d(TAG, "registerAccountForPush: started for "+accountID);
new RegisterForPushNotifications(endpoint,
standard,
p256dh,
auth,
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts, subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
subscription==null ? PushSubscription.Policy.ALL : subscription.policy) subscription==null ? PushSubscription.Policy.ALL : subscription.policy)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){

View file

@ -4,10 +4,11 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{
public RegisterForPushNotifications(String endpoint, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy){ public RegisterForPushNotifications(String endpoint, Boolean standard, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy){
super(HttpMethod.POST, "/push/subscription", PushSubscription.class); super(HttpMethod.POST, "/push/subscription", PushSubscription.class);
Request r=new Request(); Request r=new Request();
r.subscription.endpoint=endpoint; r.subscription.endpoint=endpoint;
r.subscription.standard = standard;
r.data.alerts=alerts; r.data.alerts=alerts;
r.policy=policy; r.policy=policy;
r.subscription.keys.p256dh=encryptionKey; r.subscription.keys.p256dh=encryptionKey;
@ -27,6 +28,8 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Subscription{ private static class Subscription{
public String endpoint; public String endpoint;
// Use standard push notifications if available
public Boolean standard;
public Keys keys=new Keys(); public Keys keys=new Keys();
} }

View file

@ -34,6 +34,7 @@ import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.UnifiedPushHelper;
import org.unifiedpush.android.connector.UnifiedPush; import org.unifiedpush.android.connector.UnifiedPush;
import java.io.File; import java.io.File;
@ -127,12 +128,12 @@ public class AccountSessionManager{
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri)); MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
updateMoreInstanceInfo(instance, instance.uri); updateMoreInstanceInfo(instance, instance.uri);
if (!UnifiedPush.getDistributor(context).isEmpty()) { if (UnifiedPushHelper.isUnifiedPushEnabled(context)) {
UnifiedPush.registerApp( UnifiedPush.register(
context, context,
session.getID(), session.getID(),
new ArrayList<>(), null,
context.getPackageName() session.app.vapidKey.replaceAll("=","")
); );
} else if(PushSubscriptionManager.arePushNotificationsAvailable()){ } else if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null); session.getPushSubscriptionManager().registerAccountForPush(null);

View file

@ -26,6 +26,7 @@ import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.UnifiedPushHelper;
import org.unifiedpush.android.connector.UnifiedPush; import org.unifiedpush.android.connector.UnifiedPush;
import java.time.Instant; import java.time.Instant;
@ -57,6 +58,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
// MEGALODON // MEGALODON
private boolean useUnifiedPush = false; private boolean useUnifiedPush = false;
private boolean hasAnyUnifiedPushDistrib = false;
private CheckableListItem<Void> uniformIconItem, deleteItem, onlyLatestItem, unifiedPushItem; private CheckableListItem<Void> uniformIconItem, deleteItem, onlyLatestItem, unifiedPushItem;
private CheckableListItem<Void> postsItem, updateItem; private CheckableListItem<Void> postsItem, updateItem;
@ -72,7 +74,8 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
lp=AccountSessionManager.get(accountID).getLocalPreferences(); lp=AccountSessionManager.get(accountID).getLocalPreferences();
getPushSubscription(); getPushSubscription();
useUnifiedPush=!UnifiedPush.getDistributor(getContext()).isEmpty(); useUnifiedPush=UnifiedPushHelper.isUnifiedPushEnabled(getContext());
hasAnyUnifiedPushDistrib=UnifiedPushHelper.hasAnyDistributorInstalled(getContext());
onDataLoaded(List.of( onDataLoaded(List.of(
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_fluent_alert_snooze_24_regular, i->onPauseNotificationsClick(false)), pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_fluent_alert_snooze_24_regular, i->onPauseNotificationsClick(false)),
@ -94,7 +97,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
)); ));
//only enable when distributors, who can receive notifications, are available //only enable when distributors, who can receive notifications, are available
unifiedPushItem.isEnabled=!UnifiedPush.getDistributors(getContext(), new ArrayList<>()).isEmpty(); unifiedPushItem.isEnabled=hasAnyUnifiedPushDistrib;
if (!unifiedPushItem.isEnabled) { if (!unifiedPushItem.isEnabled) {
unifiedPushItem.subtitleRes=R.string.sk_settings_unifiedpush_no_distributor_body; unifiedPushItem.subtitleRes=R.string.sk_settings_unifiedpush_no_distributor_body;
} }
@ -124,7 +127,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
GlobalUserPreferences.save(); GlobalUserPreferences.save();
lp.keepOnlyLatestNotification=onlyLatestItem.checked; lp.keepOnlyLatestNotification=onlyLatestItem.checked;
lp.save(); lp.save();
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){ if(needUpdateNotificationSettings && (PushSubscriptionManager.arePushNotificationsAvailable() || useUnifiedPush)){
ps.alerts.mention=mentionsItem.checked; ps.alerts.mention=mentionsItem.checked;
ps.alerts.reblog=boostsItem.checked; ps.alerts.reblog=boostsItem.checked;
ps.alerts.favourite=favoritesItem.checked; ps.alerts.favourite=favoritesItem.checked;
@ -316,12 +319,12 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
bannerText.setText(R.string.notifications_disabled_in_system); bannerText.setText(R.string.notifications_disabled_in_system);
bannerButton.setText(R.string.open_system_notification_settings); bannerButton.setText(R.string.open_system_notification_settings);
bannerButton.setOnClickListener(v->openSystemNotificationSettings()); bannerButton.setOnClickListener(v->openSystemNotificationSettings());
}else if(BuildConfig.BUILD_TYPE.equals("fdroidRelease") && UnifiedPush.getDistributor(getContext()).isEmpty()){ }else if(BuildConfig.BUILD_TYPE.equals("fdroidRelease") && useUnifiedPush){
bannerAdapter.setVisible(true); bannerAdapter.setVisible(true);
bannerIcon.setImageResource(R.drawable.ic_fluent_warning_24_filled); bannerIcon.setImageResource(R.drawable.ic_fluent_warning_24_filled);
bannerTitle.setVisibility(View.VISIBLE); bannerTitle.setVisibility(View.VISIBLE);
bannerTitle.setText(R.string.mo_settings_unifiedpush_warning); bannerTitle.setText(R.string.mo_settings_unifiedpush_warning);
if(UnifiedPush.getDistributors(getContext(), new ArrayList<>()).isEmpty()) { if(!hasAnyUnifiedPushDistrib) {
bannerText.setText(R.string.mo_settings_unifiedpush_warning_no_distributors); bannerText.setText(R.string.mo_settings_unifiedpush_warning_no_distributors);
bannerButton.setText(R.string.info); bannerButton.setText(R.string.info);
bannerButton.setOnClickListener(v->UiUtils.launchWebBrowser(getContext(), "https://unifiedpush.org/")); bannerButton.setOnClickListener(v->UiUtils.launchWebBrowser(getContext(), "https://unifiedpush.org/"));
@ -342,23 +345,15 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
} }
private void onUnifiedPushClick(){ private void onUnifiedPushClick(){
if(UnifiedPush.getDistributor(getContext()).isEmpty()){ if(!useUnifiedPush){
List<String> distributors = UnifiedPush.getDistributors(getContext(), new ArrayList<>()); List<String> distributors = UnifiedPush.getDistributors(getContext());
showUnifiedPushRegisterDialog(distributors); showUnifiedPushRegisterDialog(distributors);
return; return;
} }
UnifiedPushHelper.unregisterAllAccounts(getContext());
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()) {
UnifiedPush.unregisterApp(
getContext(),
accountSession.getID()
);
//re-register to fcm
accountSession.getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
}
unifiedPushItem.toggle(); unifiedPushItem.toggle();
rebindItem(unifiedPushItem); rebindItem(unifiedPushItem);
useUnifiedPush = false;
} }
private void showUnifiedPushRegisterDialog(List<String> distributors){ private void showUnifiedPushRegisterDialog(List<String> distributors){
@ -366,16 +361,10 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
(dialog, which)->{ (dialog, which)->{
String userDistrib = distributors.get(which); String userDistrib = distributors.get(which);
UnifiedPush.saveDistributor(getContext(), userDistrib); UnifiedPush.saveDistributor(getContext(), userDistrib);
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){ UnifiedPushHelper.registerAllAccounts(getContext());
UnifiedPush.registerApp(
getContext(),
accountSession.getID(),
new ArrayList<>(),
getContext().getPackageName()
);
}
unifiedPushItem.toggle(); unifiedPushItem.toggle();
rebindItem(unifiedPushItem); rebindItem(unifiedPushItem);
useUnifiedPush = true;
}).setOnCancelListener(d->rebindItem(unifiedPushItem)).show(); }).setOnCancelListener(d->rebindItem(unifiedPushItem)).show();
} }

View file

@ -0,0 +1,51 @@
package org.joinmastodon.android.utils;
import android.content.Context;
import androidx.annotation.NonNull;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.unifiedpush.android.connector.UnifiedPush;
public class UnifiedPushHelper {
/**
* @param context
* @return `true` if UnifiedPush is used
*/
public static boolean isUnifiedPushEnabled(@NonNull Context context) {
return UnifiedPush.getAckDistributor(context) != null;
}
/**
* If any distributor is installed on the device
* @param context
* @return `true` if at least one is installed
*/
public static boolean hasAnyDistributorInstalled(@NonNull Context context) {
return !UnifiedPush.getDistributors(context).isEmpty();
}
public static void registerAllAccounts(@NonNull Context context) {
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
UnifiedPush.register(
context,
accountSession.getID(),
null,
accountSession.app.vapidKey.replaceAll("=","")
);
}
}
public static void unregisterAllAccounts(@NonNull Context context) {
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
UnifiedPush.unregister(
context,
accountSession.getID()
);
// use FCM again
accountSession.getPushSubscriptionManager().registerAccountForPush(null);
}
}
}

View file

@ -2,12 +2,6 @@ pluginManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven {
url "https://www.jitpack.io"
content {
includeModule 'com.github.UnifiedPush', 'android-connector'
}
}
mavenLocal() mavenLocal()
} }
} }
@ -17,7 +11,12 @@ dependencyResolutionManagement {
google() google()
mavenCentral() mavenCentral()
mavenLocal() mavenLocal()
maven { url 'https://jitpack.io' } maven {
url 'https://jitpack.io'
content {
includeModule 'com.github.bottom-software-foundation', 'bottom-java'
}
}
} }
} }
rootProject.name = "Moshidon" rootProject.name = "Moshidon"