diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml
index 1cc532fd8..ecd97cd36 100644
--- a/mastodon/src/main/AndroidManifest.xml
+++ b/mastodon/src/main/AndroidManifest.xml
@@ -79,6 +79,7 @@
+
diff --git a/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java
new file mode 100644
index 000000000..74dbf160c
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java
@@ -0,0 +1,171 @@
+package org.joinmastodon.android;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.service.notification.StatusBarNotification;
+
+import org.joinmastodon.android.api.requests.statuses.CreateStatus;
+import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
+import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
+import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
+import org.joinmastodon.android.events.StatusCreatedEvent;
+import org.joinmastodon.android.model.Status;
+import org.joinmastodon.android.model.StatusPrivacy;
+
+import java.util.UUID;
+
+import androidx.annotation.Nullable;
+import me.grishka.appkit.api.Callback;
+import me.grishka.appkit.api.ErrorResponse;
+
+public class NotificationActionHandlerService extends Service{
+ private static final String TAG="NotificationActionHandl";
+ private int runningRequestCount=0;
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent){
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId){
+ String action=intent.getStringExtra("action");
+ String account=intent.getStringExtra("account");
+ String postID=intent.getStringExtra("post");
+ String notificationTag=intent.getStringExtra("notificationTag");
+ if(action==null || account==null || postID==null || notificationTag==null){
+ maybeStopSelf();
+ return START_NOT_STICKY;
+ }
+ NotificationManager nm=getSystemService(NotificationManager.class);
+ StatusBarNotification notification=findNotification(notificationTag);
+ if("reply".equals(action)){
+ CharSequence replyText=RemoteInput.getResultsFromIntent(intent).getCharSequence("replyText");
+ if(replyText==null){
+ maybeStopSelf();
+ return START_NOT_STICKY;
+ }
+ CreateStatus.Request req=new CreateStatus.Request();
+ req.inReplyToId=postID;
+ req.status=intent.getStringExtra("replyPrefix")+replyText;
+ req.visibility=StatusPrivacy.valueOf(intent.getStringExtra("visibility"));
+ runningRequestCount++;
+ new CreateStatus(req, UUID.randomUUID().toString())
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Status result){
+ E.post(new StatusCreatedEvent(result, account));
+ if(notification!=null){
+ Notification n=notification.getNotification();
+ nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
+ }
+ runningRequestCount--;
+ maybeStopSelf();
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ error.showToast(NotificationActionHandlerService.this);
+ if(notification!=null){
+ Notification n=notification.getNotification();
+ nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
+ }
+ runningRequestCount--;
+ maybeStopSelf();
+ }
+ })
+ .exec(account);
+ }else if("favorite".equals(action)){
+ PendingIntent prevActionIntent;
+ if(notification!=null){
+ Notification n=notification.getNotification();
+ prevActionIntent=n.actions[1].actionIntent;
+ n.actions[1].actionIntent=null;
+ n.actions[1].title=getString(R.string.button_favorited);
+ nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
+ }else{
+ prevActionIntent=null;
+ }
+ runningRequestCount++;
+ new SetStatusFavorited(postID, true)
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Status result){
+ E.post(new StatusCountersUpdatedEvent(result));
+ runningRequestCount--;
+ maybeStopSelf();
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ if(notification!=null){
+ Notification n=notification.getNotification();
+ n.actions[1].actionIntent=prevActionIntent;
+ n.actions[1].title=getString(R.string.button_favorite);
+ nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
+ }
+ error.showToast(NotificationActionHandlerService.this);
+ runningRequestCount--;
+ maybeStopSelf();
+ }
+ })
+ .exec(account);
+ }else if("boost".equals(action)){
+ PendingIntent prevActionIntent;
+ if(notification!=null){
+ Notification n=notification.getNotification();
+ prevActionIntent=n.actions[2].actionIntent;
+ n.actions[2].actionIntent=null;
+ n.actions[2].title=getString(R.string.button_reblogged);
+ nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
+ }else{
+ prevActionIntent=null;
+ }
+ runningRequestCount++;
+ new SetStatusReblogged(postID, true)
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Status result){
+ E.post(new StatusCountersUpdatedEvent(result));
+ runningRequestCount--;
+ maybeStopSelf();
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ if(notification!=null){
+ Notification n=notification.getNotification();
+ n.actions[2].actionIntent=prevActionIntent;
+ n.actions[2].title=getString(R.string.button_reblog);
+ nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n);
+ }
+ error.showToast(NotificationActionHandlerService.this);
+ runningRequestCount--;
+ maybeStopSelf();
+ }
+ })
+ .exec(account);
+ }
+ return START_NOT_STICKY;
+ }
+
+ private void maybeStopSelf(){
+ if(runningRequestCount==0)
+ stopSelf();
+ }
+
+ private StatusBarNotification findNotification(String tag){
+ for(StatusBarNotification sbn:getSystemService(NotificationManager.class).getActiveNotifications()){
+ if(tag.equals(sbn.getTag())){
+ return sbn;
+ }
+ }
+ return null;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
index af3ca3521..852431377 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
@@ -5,14 +5,15 @@ import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.RemoteInput;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
@@ -21,10 +22,13 @@ 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.Account;
+import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.PushNotification;
+import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@@ -144,19 +148,110 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setContentText(pn.body)
.setStyle(new Notification.BigTextStyle().bigText(pn.body))
.setSmallIcon(R.drawable.ic_ntf_logo)
- .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
+ .setContentIntent(PendingIntent.getActivity(context, (accountID+pn.notificationId).hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
.setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true)
+ .setOnlyAlertOnce(true)
.setLights(context.getColor(R.color.primary_700), 500, 1000)
- .setColor(context.getColor(R.color.primary_700));
+ .setColor(context.getColor(R.color.primary_700))
+ .setGroup(accountID);
if(avatar!=null){
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
}
if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){
builder.setSubText(accountName);
}
- nm.notify(accountID, NOTIFICATION_ID, builder.build());
+ String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id);
+ if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){
+ ArrayList mentions=new ArrayList<>();
+ String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
+ if(!notification.status.account.id.equals(ownID))
+ mentions.add('@'+notification.status.account.acct);
+ for(Mention mention:notification.status.mentions){
+ if(mention.id.equals(ownID))
+ continue;
+ String m='@'+mention.acct;
+ if(!mentions.contains(m))
+ mentions.add(m);
+ }
+ String replyPrefix=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
+
+ Intent replyIntent=new Intent(context, NotificationActionHandlerService.class);
+ replyIntent.putExtra("action", "reply");
+ replyIntent.putExtra("account", accountID);
+ replyIntent.putExtra("post", notification.status.id);
+ replyIntent.putExtra("notificationTag", notificationTag);
+ replyIntent.putExtra("visibility", notification.status.visibility.toString());
+ replyIntent.putExtra("replyPrefix", replyPrefix);
+ builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_reply_24px),
+ context.getString(R.string.button_reply), PendingIntent.getService(context, (accountID+pn.notificationId+"reply").hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+ .addRemoteInput(new RemoteInput.Builder("replyText").build())
+ .build());
+
+ Intent favIntent=new Intent(context, NotificationActionHandlerService.class);
+ favIntent.putExtra("action", "favorite");
+ favIntent.putExtra("account", accountID);
+ favIntent.putExtra("post", notification.status.id);
+ favIntent.putExtra("notificationTag", notificationTag);
+ builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_star_24px),
+ context.getString(R.string.button_favorite), PendingIntent.getService(context, (accountID+pn.notificationId+"favorite").hashCode(), favIntent, PendingIntent.FLAG_UPDATE_CURRENT)).build());
+
+ PendingIntent boostActionIntent;
+ if(notification.status.visibility!=StatusPrivacy.DIRECT){
+ Intent boostIntent=new Intent(context, NotificationActionHandlerService.class);
+ boostIntent.putExtra("action", "boost");
+ boostIntent.putExtra("account", accountID);
+ boostIntent.putExtra("post", notification.status.id);
+ boostIntent.putExtra("notificationTag", notificationTag);
+ boostActionIntent=PendingIntent.getService(context, (accountID+pn.notificationId+"boost").hashCode(), boostIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }else{
+ boostActionIntent=null;
+ }
+ builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_boost_24px),
+ context.getString(R.string.button_reblog), boostActionIntent).build());
+ }
+ nm.notify(notificationTag, NOTIFICATION_ID, builder.build());
+
+ StatusBarNotification[] activeNotifications=nm.getActiveNotifications();
+ ArrayList summaryLines=new ArrayList<>();
+ for(StatusBarNotification sbn:activeNotifications){
+ String tag=sbn.getTag();
+ if(tag!=null && tag.startsWith(accountID+"_")){
+ if((sbn.getNotification().flags & Notification.FLAG_GROUP_SUMMARY)==0){
+ summaryLines.add(sbn.getNotification().extras.getString("android.title"));
+ if(summaryLines.size()==5)
+ break;
+ }
+ }
+ }
+
+ if(summaryLines.size()>1){
+ Notification.Builder summaryBuilder;
+ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
+ summaryBuilder=new Notification.Builder(context, accountID+"_"+pn.notificationType);
+ }else{
+ summaryBuilder=new Notification.Builder(context)
+ .setPriority(Notification.PRIORITY_DEFAULT);
+ }
+ Notification.InboxStyle inboxStyle=new Notification.InboxStyle();
+ for(String line:summaryLines){
+ inboxStyle.addLine(line);
+ }
+ summaryBuilder.setContentTitle("content title")
+ .setContentText("content text")
+ .setSmallIcon(R.drawable.ic_ntf_logo)
+ .setColor(context.getColor(R.color.primary_700))
+ .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
+ .setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
+ .setShowWhen(true)
+ .setCategory(Notification.CATEGORY_SOCIAL)
+ .setAutoCancel(true)
+ .setGroup(accountID)
+ .setGroupSummary(true)
+ .setStyle(inboxStyle.setSummaryText(accountName));
+ nm.notify(accountID+"_summary", NOTIFICATION_ID, summaryBuilder.build());
+ }
}
}