Prechádzať zdrojové kódy

Merge pull request #3608 from nextcloud/actionsInNotifications

Actions in notifications
Tobias Kaminsky 6 rokov pred
rodič
commit
cd0b0fb552

+ 5 - 0
drawable_resources/check_circle.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <path d="M0 0h24v24H0z" fill="none" />
+    <path
+        d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
+</svg>

+ 5 - 0
drawable_resources/check_circle_outline.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+    <path fill="none" d="M0 0h24v24H0V0zm0 0h24v24H0V0z" />
+    <path
+        d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" />
+</svg>

+ 149 - 19
src/main/java/com/owncloud/android/jobs/NotificationJob.java

@@ -45,13 +45,25 @@ import com.owncloud.android.datamodel.SignatureVerification;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.notifications.DeleteNotificationRemoteOperation;
+import com.owncloud.android.lib.resources.notifications.GetNotificationRemoteOperation;
+import com.owncloud.android.lib.resources.notifications.models.Action;
+import com.owncloud.android.lib.resources.notifications.models.Notification;
 import com.owncloud.android.ui.activity.NotificationsActivity;
 import com.owncloud.android.ui.notifications.NotificationUtils;
 import com.owncloud.android.utils.PushUtils;
 import com.owncloud.android.utils.ThemeUtils;
 
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+
 import java.io.IOException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
@@ -64,23 +76,26 @@ import javax.crypto.NoSuchPaddingException;
 import androidx.annotation.NonNull;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationManagerCompat;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 public class NotificationJob extends Job {
     public static final String TAG = "NotificationJob";
 
+    public static final String KEY_NOTIFICATION_ACCOUNT = "KEY_NOTIFICATION_ACCOUNT";
     public static final String KEY_NOTIFICATION_SUBJECT = "subject";
     public static final String KEY_NOTIFICATION_SIGNATURE = "signature";
-    public static final String KEY_NOTIFICATION_ACCOUNT = "KEY_NOTIFICATION_ACCOUNT";
+    private static final String KEY_NOTIFICATION_ACTION_LINK = "KEY_NOTIFICATION_ACTION_LINK";
+    private static final String KEY_NOTIFICATION_ACTION_TYPE = "KEY_NOTIFICATION_ACTION_TYPE";
     private static final String PUSH_NOTIFICATION_ID = "PUSH_NOTIFICATION_ID";
     private static final String NUMERIC_NOTIFICATION_ID = "NUMERIC_NOTIFICATION_ID";
 
     private SecureRandom randomId = new SecureRandom();
+    private Context context;
 
     @NonNull
     @Override
     protected Result onRunJob(@NonNull Params params) {
-
-        Context context = getContext();
+        context = getContext();
         PersistableBundleCompat persistableBundleCompat = getParams().getExtras();
         String subject = persistableBundleCompat.getString(KEY_NOTIFICATION_SUBJECT, "");
         String signature = persistableBundleCompat.getString(KEY_NOTIFICATION_SIGNATURE, "");
@@ -96,7 +111,7 @@ public class NotificationJob extends Job {
                                                                                             base64DecodedSignature,
                                                                                             base64DecodedSubject);
 
-                    if (signatureVerification.isSignatureValid()) {
+                    if (signatureVerification != null && signatureVerification.isSignatureValid()) {
                         Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
                         cipher.init(Cipher.DECRYPT_MODE, privateKey);
                         byte[] decryptedSubject = cipher.doFinal(base64DecodedSubject);
@@ -107,7 +122,7 @@ public class NotificationJob extends Job {
 
                         // We ignore Spreed messages for now
                         if (!"spreed".equals(decryptedPushMessage.getApp())) {
-                            sendNotification(decryptedPushMessage, signatureVerification.getAccount());
+                            fetchCompleteNotification(signatureVerification.getAccount(), decryptedPushMessage);
                         }
                     }
                 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e1) {
@@ -121,9 +136,8 @@ public class NotificationJob extends Job {
         return Result.SUCCESS;
     }
 
-    private void sendNotification(DecryptedPushMessage pushMessage, Account account) {
-        Context context = getContext();
-        Intent intent = new Intent(getContext(), NotificationsActivity.class);
+    private void sendNotification(Notification notification, Account account) {
+        Intent intent = new Intent(context, NotificationsActivity.class);
         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         intent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
         PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
@@ -136,26 +150,95 @@ public class NotificationJob extends Job {
                 .setColor(ThemeUtils.primaryColor(account, false, context))
                 .setShowWhen(true)
                 .setSubText(account.name)
-                .setContentTitle(pushMessage.getSubject())
+                .setContentTitle(notification.getSubject())
+                .setContentText(notification.getMessage())
                 .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
                 .setAutoCancel(true)
+                .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
                 .setContentIntent(pendingIntent);
 
-        Intent disableDetection = new Intent(context, NotificationJob.NotificationReceiver.class);
-        disableDetection.putExtra(NUMERIC_NOTIFICATION_ID, pushMessage.getNid());
-        disableDetection.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId);
-        disableDetection.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
+        // Remove
+        if (notification.getActions().isEmpty()) {
+            Intent disableDetection = new Intent(context, NotificationJob.NotificationReceiver.class);
+            disableDetection.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId());
+            disableDetection.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId);
+            disableDetection.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
+
+            PendingIntent disableIntent = PendingIntent.getBroadcast(context, pushNotificationId, disableDetection,
+                                                                     PendingIntent.FLAG_CANCEL_CURRENT);
+
+            notificationBuilder.addAction(new NotificationCompat.Action(R.drawable.ic_close,
+                                                                        context.getString(R.string.remove_push_notification), disableIntent));
+        } else {
+            // Actions
+            for (Action action : notification.getActions()) {
+                Intent actionIntent = new Intent(context, NotificationJob.NotificationReceiver.class);
+                actionIntent.putExtra(NUMERIC_NOTIFICATION_ID, notification.getNotificationId());
+                actionIntent.putExtra(PUSH_NOTIFICATION_ID, pushNotificationId);
+                actionIntent.putExtra(KEY_NOTIFICATION_ACCOUNT, account.name);
+                actionIntent.putExtra(KEY_NOTIFICATION_ACTION_LINK, action.link);
+                actionIntent.putExtra(KEY_NOTIFICATION_ACTION_TYPE, action.type);
+
+
+                PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, randomId.nextInt(),
+                                                                               actionIntent,
+                                                                               PendingIntent.FLAG_CANCEL_CURRENT);
+
+                int icon;
+                if (action.primary) {
+                    icon = R.drawable.ic_check_circle;
+                } else {
+                    icon = R.drawable.ic_check_circle_outline;
+                }
 
-        PendingIntent disableIntent = PendingIntent.getBroadcast(context, pushNotificationId, disableDetection,
-                                                                 PendingIntent.FLAG_CANCEL_CURRENT);
+                notificationBuilder.addAction(new NotificationCompat.Action(icon, action.label, actionPendingIntent));
+            }
+        }
 
-        notificationBuilder.addAction(new NotificationCompat.Action(R.drawable.ic_close,
-                                                                    context.getString(R.string.remove_push_notification), disableIntent));
+        notificationBuilder.setPublicVersion(
+            new NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
+                .setSmallIcon(R.drawable.notification_icon)
+                .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
+                .setColor(ThemeUtils.primaryColor(account, false, context))
+                .setShowWhen(true)
+                .setSubText(account.name)
+                .setContentTitle(context.getString(R.string.new_notification))
+                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+                .setAutoCancel(true)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setContentIntent(pendingIntent).build());
 
         NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
         notificationManager.notify(pushNotificationId, notificationBuilder.build());
     }
 
+    private void fetchCompleteNotification(Account account, DecryptedPushMessage decryptedPushMessage) {
+        Account currentAccount = AccountUtils.getOwnCloudAccountByName(context, account.name);
+
+        if (currentAccount == null) {
+            Log_OC.e(this, "Account may not be null");
+            return;
+        }
+
+        try {
+            OwnCloudAccount ocAccount = new OwnCloudAccount(currentAccount, context);
+            OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton()
+                .getClientFor(ocAccount, context);
+            client.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
+
+            RemoteOperationResult result = new GetNotificationRemoteOperation(decryptedPushMessage.nid)
+                .execute(client);
+
+            if (result.isSuccess()) {
+                Notification notification = result.getNotificationData().get(0);
+                sendNotification(notification, account);
+            }
+
+        } catch (Exception e) {
+            Log_OC.e(this, "Error creating account", e);
+        }
+    }
+
     public static class NotificationReceiver extends BroadcastReceiver {
 
         @Override
@@ -179,10 +262,20 @@ public class NotificationJob extends Job {
                             .getClientFor(ocAccount, context);
                         client.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
 
-                        new DeleteNotificationRemoteOperation(numericNotificationId).execute(client);
+                        String actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE);
+                        String actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK);
 
-                        cancel(context, pushNotificationId);
+                        boolean success;
+                        if (!TextUtils.isEmpty(actionType) && !TextUtils.isEmpty(actionLink)) {
+                            success = executeAction(actionType, actionLink, client) == HttpStatus.SC_OK;
+                        } else {
+                            success = new DeleteNotificationRemoteOperation(numericNotificationId)
+                                .execute(client).isSuccess();
+                        }
 
+                        if (success) {
+                            cancel(context, pushNotificationId);
+                        }
                     } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException |
                         IOException | OperationCanceledException | AuthenticatorException e) {
                         Log_OC.e(TAG, "Error initializing client", e);
@@ -191,6 +284,43 @@ public class NotificationJob extends Job {
             }
         }
 
+        @SuppressFBWarnings(value = "HTTP_PARAMETER_POLLUTION",
+            justification = "link and type are from server and expected to be safe")
+        private int executeAction(String actionType, String actionLink, OwnCloudClient client) {
+            HttpMethod method;
+
+            switch (actionType) {
+                case "GET":
+                    method = new GetMethod(actionLink);
+                    break;
+
+                case "POST":
+                    method = new PostMethod(actionLink);
+                    break;
+
+                case "DELETE":
+                    method = new DeleteMethod(actionLink);
+                    break;
+
+                case "PUT":
+                    method = new PutMethod(actionLink);
+                    break;
+
+                default:
+                    // do nothing
+                    return 0;
+            }
+
+            method.setRequestHeader(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE);
+
+            try {
+                return client.executeMethod(method);
+            } catch (IOException e) {
+                Log_OC.e(TAG, "Execution of notification action failed: " + e);
+            }
+            return 0;
+        }
+
         private void cancel(Context context, int notificationId) {
             NotificationManager notificationManager = (NotificationManager) context.getSystemService(
                 Activity.NOTIFICATION_SERVICE);

+ 5 - 0
src/main/java/com/owncloud/android/ui/asynctasks/NotificationExecuteActionTask.java

@@ -14,6 +14,7 @@ import org.apache.commons.httpclient.HttpStatus;
 import org.apache.commons.httpclient.methods.DeleteMethod;
 import org.apache.commons.httpclient.methods.GetMethod;
 import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
 
 import java.io.IOException;
 
@@ -48,6 +49,10 @@ public class NotificationExecuteActionTask extends AsyncTask<Action, Void, Boole
                 method = new DeleteMethod(action.link);
                 break;
 
+            case "PUT":
+                method = new PutMethod(action.link);
+                break;
+
             default:
                 // do nothing
                 return false;

+ 9 - 0
src/main/res/drawable/ic_check_circle.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
+</vector>

+ 9 - 0
src/main/res/drawable/ic_check_circle_outline.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M16.59,7.58L10,14.17l-3.59,-3.58L5,12l5,5 8,-8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
+</vector>

+ 1 - 0
src/main/res/values/strings.xml

@@ -861,4 +861,5 @@
     <string name="remove_notification_failed">Failed to remove notification.</string>
     <string name="notification_action_failed">Failed to execute action.</string>
     <string name="remove_push_notification">Remove</string>
+    <string name="new_notification">New Notification</string>
 </resources>