Эх сурвалжийг харах

Merge pull request #5825 from nextcloud/pushDeck

send push to deck app, if installed
Tobias Kaminsky 5 жил өмнө
parent
commit
30501e9503

+ 177 - 0
src/androidTest/java/com/nextcloud/client/integration/deck/DeckApiTest.kt

@@ -0,0 +1,177 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.integration.deck
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.nextcloud.client.account.User
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.anyOrNull
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.times
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.whenever
+import com.owncloud.android.lib.resources.notifications.models.Notification
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Suite
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(Suite::class)
+@Suite.SuiteClasses(
+    DeckApiTest.DeckIsInstalled::class,
+    DeckApiTest.DeckIsNotInstalled::class
+)
+class DeckApiTest {
+
+    abstract class Fixture {
+        @Mock
+        lateinit var packageManager: PackageManager
+
+        lateinit var context: Context
+
+        @Mock
+        lateinit var user: User
+
+        lateinit var deck: DeckApiImpl
+
+        @Before
+        fun setUpFixture() {
+            MockitoAnnotations.initMocks(this)
+            context = InstrumentationRegistry.getInstrumentation().targetContext
+            deck = DeckApiImpl(context, packageManager)
+        }
+    }
+
+    @RunWith(Parameterized::class)
+    class DeckIsInstalled : Fixture() {
+
+        @Parameterized.Parameter(0)
+        lateinit var installedDeckPackage: String
+
+        companion object {
+            @Parameterized.Parameters
+            @JvmStatic
+            fun initParametrs(): Array<String> {
+                return DeckApiImpl.DECK_APP_PACKAGES
+            }
+        }
+
+        @Before
+        fun setUp() {
+            whenever(packageManager.resolveActivity(any(), any())).thenAnswer {
+                val intent = it.getArgument<Intent>(0)
+                return@thenAnswer if (intent.component?.packageName == installedDeckPackage) {
+                    ResolveInfo()
+                } else {
+                    null
+                }
+            }
+        }
+
+        @Test
+        fun can_forward_deck_notification() {
+            // GIVEN
+            //      notification to deck arrives
+            val notification = Notification().apply { app = "deck" }
+
+            // WHEN
+            //      deck action is created
+            val forwardActionIntent = deck.createForwardToDeckActionIntent(notification, user)
+
+            // THEN
+            //      open action is created
+            assertTrue("Failed for $installedDeckPackage", forwardActionIntent.isPresent)
+        }
+
+        @Test
+        fun notifications_from_other_apps_are_ignored() {
+            // GIVEN
+            //      notification from other app arrives
+            val deckNotification = Notification().apply {
+                app = "some_other_app"
+            }
+
+            // WHEN
+            //      deck action is created
+            val openDeckActionIntent = deck.createForwardToDeckActionIntent(deckNotification, user)
+
+            // THEN
+            //      deck application is not being resolved
+            //      open action is not created
+            verify(packageManager, never()).resolveActivity(anyOrNull(), anyOrNull())
+            assertFalse(openDeckActionIntent.isPresent)
+        }
+    }
+
+    class DeckIsNotInstalled : Fixture() {
+
+        @Before
+        fun setUp() {
+            whenever(packageManager.resolveActivity(any(), any())).thenReturn(null)
+        }
+
+        @Test
+        fun cannot_forward_deck_notification() {
+            // GIVEN
+            //      notification is coming from deck app
+            val notification = Notification().apply {
+                app = DeckApiImpl.APP_NAME
+            }
+
+            // WHEN
+            //      creating open in deck action
+            val openDeckActionIntent = deck.createForwardToDeckActionIntent(notification, user)
+
+            // THEN
+            //      deck application is being resolved using all known packages
+            //      open action is not created
+            verify(packageManager, times(DeckApiImpl.DECK_APP_PACKAGES.size)).resolveActivity(anyOrNull(), anyOrNull())
+            assertFalse(openDeckActionIntent.isPresent)
+        }
+
+        @Test
+        fun notifications_from_other_apps_are_ignored() {
+            // GIVEN
+            //      notification is coming from other app
+            val notification = Notification().apply {
+                app = "some_other_app"
+            }
+
+            // WHEN
+            //      creating open in deck action
+            val openDeckActionIntent = deck.createForwardToDeckActionIntent(notification, user)
+
+            // THEN
+            //      deck application is not being resolved
+            //      open action is not created
+            verify(packageManager, never()).resolveActivity(anyOrNull(), anyOrNull())
+            assertFalse(openDeckActionIntent.isPresent)
+        }
+    }
+}

+ 49 - 0
src/main/java/com/nextcloud/client/integration/deck/DeckApi.java

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud application
+ *
+ * @author Stefan Niedermann
+ * Copyright (C) 2020 Stefan Niedermann <info@niedermann.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.integration.deck;
+
+import android.app.PendingIntent;
+
+import com.nextcloud.client.account.User;
+import com.nextcloud.java.util.Optional;
+import com.owncloud.android.lib.resources.notifications.models.Notification;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This API is for an integration with the <a href="https://github.com/stefan-niedermann/nextcloud-deck">Nextcloud
+ * Deck</a> app for android.
+ */
+public interface DeckApi {
+
+    /**
+     * Creates a PendingIntent that can be used in a NotificationBuilder to open the notification link in Deck app
+     *
+     * @param notification Notification Notification that could be forwarded to Deck
+     * @param user         The user that is affected by the notification
+     * @return If notification can be consumed by Deck, a PendingIntent opening notification link in Deck app; empty
+     * value otherwise
+     * @see <a href="https://apps.nextcloud.com/apps/deck">Deck Server App</a>
+     */
+    @NonNull
+    Optional<PendingIntent> createForwardToDeckActionIntent(@NonNull final Notification notification,
+                                                            @NonNull final User user);
+}

+ 97 - 0
src/main/java/com/nextcloud/client/integration/deck/DeckApiImpl.java

@@ -0,0 +1,97 @@
+/*
+ * Nextcloud application
+ *
+ * @author Stefan Niedermann
+ * Copyright (C) 2020 Stefan Niedermann <info@niedermann.it>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.integration.deck;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+
+import com.nextcloud.client.account.User;
+import com.nextcloud.java.util.Optional;
+import com.owncloud.android.lib.resources.notifications.models.Notification;
+
+import androidx.annotation.NonNull;
+
+public class DeckApiImpl implements DeckApi {
+
+    private static final String TAG = DeckApiImpl.class.getSimpleName();
+
+    static final String APP_NAME = "deck";
+    static final String[] DECK_APP_PACKAGES = new String[] {
+        "it.niedermann.nextcloud.deck",
+        "it.niedermann.nextcloud.deck.play",
+        "it.niedermann.nextcloud.deck.dev"
+    };
+    static final String DECK_ACTIVITY_TO_START = "it.niedermann.nextcloud.deck.ui.PushNotificationActivity";
+
+    private static final String EXTRA_ACCOUNT = "account";
+    private static final String EXTRA_LINK = "link";
+    private static final String EXTRA_OBJECT_ID = "objectId";
+    private static final String EXTRA_SUBJECT = "subject";
+    private static final String EXTRA_SUBJECT_RICH = "subjectRich";
+    private static final String EXTRA_MESSAGE = "message";
+    private static final String EXTRA_MESSAGE_RICH = "messageRich";
+    private static final String EXTRA_USER = "user";
+    private static final String EXTRA_NID = "nid";
+
+    private final Context context;
+    private final PackageManager packageManager;
+
+    public DeckApiImpl(@NonNull Context context, @NonNull PackageManager packageManager) {
+        this.context = context;
+        this.packageManager = packageManager;
+    }
+
+    @NonNull
+    @Override
+    public Optional<PendingIntent> createForwardToDeckActionIntent(@NonNull Notification notification, @NonNull User user) {
+        if (APP_NAME.equalsIgnoreCase(notification.app)) {
+            final Intent intent = new Intent();
+            for (String appPackage : DECK_APP_PACKAGES) {
+                intent.setClassName(appPackage, DECK_ACTIVITY_TO_START);
+                if (packageManager.resolveActivity(intent, 0) != null) {
+                    return Optional.of(createPendingIntent(intent, notification, user));
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
+    private PendingIntent createPendingIntent(@NonNull Intent intent, @NonNull Notification notification, @NonNull User user) {
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        return PendingIntent.getActivity(context, 0, putExtrasToIntent(intent, notification, user),
+                                         PendingIntent.FLAG_ONE_SHOT);
+    }
+
+    private Intent putExtrasToIntent(@NonNull Intent intent, @NonNull Notification notification, @NonNull User user) {
+        return intent
+            .putExtra(EXTRA_ACCOUNT, user.getAccountName())
+            .putExtra(EXTRA_LINK, notification.getLink())
+            .putExtra(EXTRA_OBJECT_ID, notification.getObjectId())
+            .putExtra(EXTRA_SUBJECT, notification.getSubject())
+            .putExtra(EXTRA_SUBJECT_RICH, notification.getSubjectRich())
+            .putExtra(EXTRA_MESSAGE, notification.getMessage())
+            .putExtra(EXTRA_MESSAGE_RICH, notification.getMessageRich())
+            .putExtra(EXTRA_USER, notification.getUser())
+            .putExtra(EXTRA_NID, notification.getNotificationId());
+    }
+}

+ 20 - 9
src/main/java/com/nextcloud/client/jobs/NotificationWork.kt

@@ -42,6 +42,7 @@ import androidx.work.WorkerParameters
 import com.google.gson.Gson
 import com.nextcloud.client.account.User
 import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.integration.deck.DeckApiImpl
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.DecryptedPushMessage
 import com.owncloud.android.lib.common.OwnCloudClient
@@ -133,17 +134,27 @@ class NotificationWork constructor(
     private fun sendNotification(notification: Notification, user: User) {
         val randomId = SecureRandom()
         val file = notification.subjectRichParameters["file"]
-        val intent: Intent
-        if (file == null) {
-            intent = Intent(context, NotificationsActivity::class.java)
+
+        val deckApi = DeckApiImpl(context, context.packageManager)
+        val deckActionOverrideIntent = deckApi.createForwardToDeckActionIntent(notification, user)
+
+        val pendingIntent: PendingIntent
+        if (deckActionOverrideIntent.isPresent) {
+            pendingIntent = deckActionOverrideIntent.get()
         } else {
-            intent = Intent(context, FileDisplayActivity::class.java)
-            intent.action = Intent.ACTION_VIEW
-            intent.putExtra(FileDisplayActivity.KEY_FILE_ID, file.id)
+            val intent: Intent
+            if (file == null) {
+                intent = Intent(context, NotificationsActivity::class.java)
+            } else {
+                intent = Intent(context, FileDisplayActivity::class.java)
+                intent.action = Intent.ACTION_VIEW
+                intent.putExtra(FileDisplayActivity.KEY_FILE_ID, file.id)
+            }
+            intent.putExtra(KEY_NOTIFICATION_ACCOUNT, user.accountName)
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+            pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
         }
-        intent.putExtra(KEY_NOTIFICATION_ACCOUNT, user.accountName)
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
-        val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
+
         val pushNotificationId = randomId.nextInt()
         val notificationBuilder = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_PUSH)
             .setSmallIcon(R.drawable.notification_icon)