Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unifiedpush/push settings #589

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion mastodon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ dependencies {
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
annotationProcessor 'org.parceler:parceler:1.1.12'
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.ext:junit:1.1.5'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.webkit.WebView;

import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.utils.UnifiedPushHelper;

import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.NetworkUtils;
Expand All @@ -27,7 +28,11 @@ public void onCreate(){
ImageCache.setParams(params);
NetworkUtils.setUserAgent("MoshidonAndroid/"+BuildConfig.VERSION_NAME);

PushSubscriptionManager.tryRegisterFCM();
if (UnifiedPushHelper.isUnifiedPushEnabled(this)){
UnifiedPushHelper.registerAllAccounts(this);
} else {
PushSubscriptionManager.tryRegisterFCM();
}
GlobalUserPreferences.load();
if(BuildConfig.DEBUG){
WebView.setWebContentsDebuggingEnabled(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ public void notifyUnifiedPush(Context context, AccountSession account, org.joinm
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);
AccountSession session=AccountSessionManager.get(accountID);
Account self=session.self;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@

import org.jetbrains.annotations.NotNull;
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.AccountSessionManager;
import org.joinmastodon.android.model.Notification;
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.data.PublicKeySet;
import org.unifiedpush.android.connector.data.PushEndpoint;
import org.unifiedpush.android.connector.data.PushMessage;

import java.util.List;
import java.util.function.Function;

import kotlin.text.Charsets;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;

Expand All @@ -24,16 +32,23 @@ public UnifiedPushNotificationReceiver() {
}

@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
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);
if (account != null)
account.getPushSubscriptionManager().registerAccountForPush(null, endpoint);
if (account != null) {
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
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
Log.d(TAG, "onRegistrationFailed: " + instance);
//re-register for gcm
Expand All @@ -53,26 +68,46 @@ public void onUnregistered(@NotNull Context context, @NotNull String instance) {
}

@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
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);

if (account == null)
return;

//this is stupid
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush,
// thus it is not possible to decrypt them. SO we need to re-request them from the server and transform them later on
// The official uses fcm and moves the headers to extra data, see
// https://github.com/mastodon/webpush-fcm-relay/blob/cac95b28d5364b0204f629283141ac3fb749e0c5/webpush-fcm-relay.go#L116
// https://github.com/tuskyapp/Tusky/pull/2303#issue-1112080540
if (message.getDecrypted()) {
// If the mastodon server supports the standard webpush, we can directly use the content
Log.d(TAG, "Push message correctly decrypted");
PushNotification pn = MastodonAPIController.gson.fromJson(new String(message.getContent(), Charsets.UTF_8), PushNotification.class);
new GetNotificationByID(pn.notificationId)
.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<>(){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
result.items
.stream()
.findFirst()
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, account, value)));
.ifPresent(value->MastodonAPIController.runInBackground(callback.apply(value)));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,23 @@ public void registerAccountForPush(PushSubscription subscription, String endpoin

//work-around for adding the randomAccountId
String newEndpoint = endpoint;
if (endpoint.startsWith("https://app.joinmastodon.org/relay-to/fcm/"))
newEndpoint += pushAccountID;
Boolean standard = true;
if (endpoint.startsWith("https://app.joinmastodon.org/relay-to/fcm/")){
newEndpoint+=pushAccountID;
standard = false;
}

registerAccountForPush(subscription, standard, newEndpoint, encodedPublicKey, encodedAuthKey);
});
}

new RegisterForPushNotifications(newEndpoint,
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.Policy.ALL : subscription.policy)
.setCallback(new Callback<>(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import org.joinmastodon.android.model.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);
Request r=new Request();
r.subscription.endpoint=endpoint;
r.subscription.standard = standard;
r.data.alerts=alerts;
r.policy=policy;
r.subscription.keys.p256dh=encryptionKey;
Expand All @@ -27,6 +28,8 @@ private static class Keys{

private static class Subscription{
public String endpoint;
// Use standard push notifications if available
public Boolean standard;
public Keys keys=new Keys();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.UnifiedPushHelper;
import org.unifiedpush.android.connector.UnifiedPush;

import java.io.File;
Expand Down Expand Up @@ -127,12 +128,12 @@ public void addAccount(Instance instance, Token token, Account self, Application
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));

updateMoreInstanceInfo(instance, instance.uri);
if (!UnifiedPush.getDistributor(context).isEmpty()) {
UnifiedPush.registerApp(
if (UnifiedPushHelper.isUnifiedPushEnabled(context)) {
UnifiedPush.register(
context,
session.getID(),
new ArrayList<>(),
context.getPackageName()
null,
session.app.vapidKey.replaceAll("=","")
);
} else if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.UnifiedPushHelper;
import org.unifiedpush.android.connector.UnifiedPush;

import java.time.Instant;
Expand Down Expand Up @@ -57,6 +58,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{

// MEGALODON
private boolean useUnifiedPush = false;
private boolean hasAnyUnifiedPushDistrib = false;
private CheckableListItem<Void> uniformIconItem, deleteItem, onlyLatestItem, unifiedPushItem;
private CheckableListItem<Void> postsItem, updateItem;

Expand All @@ -72,7 +74,8 @@ public void onCreate(Bundle savedInstanceState){
lp=AccountSessionManager.get(accountID).getLocalPreferences();

getPushSubscription();
useUnifiedPush=!UnifiedPush.getDistributor(getContext()).isEmpty();
useUnifiedPush=UnifiedPushHelper.isUnifiedPushEnabled(getContext());
hasAnyUnifiedPushDistrib=UnifiedPushHelper.hasAnyDistributorInstalled(getContext());

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)),
Expand All @@ -94,7 +97,7 @@ public void onCreate(Bundle savedInstanceState){
));

//only enable when distributors, who can receive notifications, are available
unifiedPushItem.isEnabled=!UnifiedPush.getDistributors(getContext(), new ArrayList<>()).isEmpty();
unifiedPushItem.isEnabled=hasAnyUnifiedPushDistrib;
if (!unifiedPushItem.isEnabled) {
unifiedPushItem.subtitleRes=R.string.sk_settings_unifiedpush_no_distributor_body;
}
Expand Down Expand Up @@ -124,7 +127,7 @@ protected void onHidden(){
GlobalUserPreferences.save();
lp.keepOnlyLatestNotification=onlyLatestItem.checked;
lp.save();
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
if(needUpdateNotificationSettings && (PushSubscriptionManager.arePushNotificationsAvailable() || useUnifiedPush)){
ps.alerts.mention=mentionsItem.checked;
ps.alerts.reblog=boostsItem.checked;
ps.alerts.favourite=favoritesItem.checked;
Expand Down Expand Up @@ -316,12 +319,12 @@ private void updateBanner(){
bannerText.setText(R.string.notifications_disabled_in_system);
bannerButton.setText(R.string.open_system_notification_settings);
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);
bannerIcon.setImageResource(R.drawable.ic_fluent_warning_24_filled);
bannerTitle.setVisibility(View.VISIBLE);
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);
bannerButton.setText(R.string.info);
bannerButton.setOnClickListener(v->UiUtils.launchWebBrowser(getContext(), "https://unifiedpush.org/"));
Expand All @@ -342,40 +345,26 @@ private void updateBanner(){
}

private void onUnifiedPushClick(){
if(UnifiedPush.getDistributor(getContext()).isEmpty()){
List<String> distributors = UnifiedPush.getDistributors(getContext(), new ArrayList<>());
if(!useUnifiedPush){
List<String> distributors = UnifiedPush.getDistributors(getContext());
showUnifiedPushRegisterDialog(distributors);
return;
}

for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()) {
UnifiedPush.unregisterApp(
getContext(),
accountSession.getID()
);

//re-register to fcm
accountSession.getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
}
UnifiedPushHelper.unregisterAllAccounts(getContext());
unifiedPushItem.toggle();
rebindItem(unifiedPushItem);
useUnifiedPush = false;
}

private void showUnifiedPushRegisterDialog(List<String> distributors){
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_unifiedpush_choose).setItems(distributors.toArray(String[]::new),
(dialog, which)->{
String userDistrib = distributors.get(which);
UnifiedPush.saveDistributor(getContext(), userDistrib);
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
UnifiedPush.registerApp(
getContext(),
accountSession.getID(),
new ArrayList<>(),
getContext().getPackageName()
);
}
UnifiedPushHelper.registerAllAccounts(getContext());
unifiedPushItem.toggle();
rebindItem(unifiedPushItem);
useUnifiedPush = true;
}).setOnCancelListener(d->rebindItem(unifiedPushItem)).show();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading