1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * vim: ts=4 sw=4 expandtab:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 package org.mozilla.geckoview;
8 
9 import org.mozilla.gecko.EventDispatcher;
10 import org.mozilla.gecko.GeckoThread;
11 import org.mozilla.gecko.util.BundleEventListener;
12 import org.mozilla.gecko.util.EventCallback;
13 import org.mozilla.gecko.util.GeckoBundle;
14 import org.mozilla.gecko.util.ThreadUtils;
15 
16 import androidx.annotation.NonNull;
17 import androidx.annotation.Nullable;
18 import androidx.annotation.UiThread;
19 import android.util.Log;
20 
21 public class WebPushController {
22     private static final String LOGTAG = "WebPushController";
23 
24     private WebPushDelegate mDelegate;
25     private BundleEventListener mEventListener;
26 
WebPushController()27     /* package */ WebPushController() {
28         mEventListener = new EventListener();
29         EventDispatcher.getInstance().registerUiThreadListener(mEventListener,
30                 "GeckoView:PushSubscribe",
31                 "GeckoView:PushUnsubscribe",
32                 "GeckoView:PushGetSubscription");
33     }
34 
35     /**
36      * Sets the {@link WebPushDelegate} for this instance.
37      *
38      * @param delegate The {@link WebPushDelegate} instance.
39      */
40     @UiThread
setDelegate(final @Nullable WebPushDelegate delegate)41     public void setDelegate(final @Nullable WebPushDelegate delegate) {
42         ThreadUtils.assertOnUiThread();
43         mDelegate = delegate;
44     }
45 
46     /**
47      * Send a push event for a given subscription.
48      *
49      * @param scope The Service Worker scope associated with this subscription.
50      */
51     @UiThread
onPushEvent(final @NonNull String scope)52     public void onPushEvent(final @NonNull String scope) {
53         ThreadUtils.assertOnUiThread();
54         onPushEvent(scope, null);
55     }
56 
57     /**
58      * Send a push event with a payload for a given subscription.
59      * @param scope The Service Worker scope associated with this subscription.
60      * @param data The unencrypted payload.
61      */
62     @UiThread
onPushEvent(final @NonNull String scope, final @Nullable byte[] data)63     public void onPushEvent(final @NonNull String scope, final @Nullable byte[] data) {
64         ThreadUtils.assertOnUiThread();
65 
66         GeckoThread.waitForState(GeckoThread.State.JNI_READY).accept(val -> {
67             final GeckoBundle msg = new GeckoBundle(2);
68             msg.putString("scope", scope);
69             msg.putString("data", Base64Utils.encode(data));
70             EventDispatcher.getInstance().dispatch("GeckoView:PushEvent", msg);
71         }, e -> Log.e(LOGTAG, "Unable to deliver Web Push message", e));
72     }
73 
74     /**
75      * Notify that a given subscription has changed. This is normally a signal to the content
76      * that it needs to re-subscribe.
77      *
78      * @param scope The Service Worker scope associated with this subscription.
79      */
80     @UiThread
onSubscriptionChanged(final @NonNull String scope)81     public void onSubscriptionChanged(final @NonNull String scope) {
82         ThreadUtils.assertOnUiThread();
83 
84         final GeckoBundle msg = new GeckoBundle(1);
85         msg.putString("scope", scope);
86         EventDispatcher.getInstance().dispatch("GeckoView:PushSubscriptionChanged", msg);
87     }
88 
89     private class EventListener implements BundleEventListener {
90 
91         @Override
handleMessage(final String event, final GeckoBundle message, final EventCallback callback)92         public void handleMessage(final String event, final GeckoBundle message, final EventCallback callback) {
93             if (mDelegate == null) {
94                 callback.sendError("Not allowed");
95                 return;
96             }
97 
98             switch (event) {
99                 case "GeckoView:PushSubscribe": {
100                     byte[] appServerKey = null;
101                     if (message.containsKey("appServerKey")) {
102                         appServerKey = Base64Utils.decode(message.getString("appServerKey"));
103                     }
104 
105                     final GeckoResult<WebPushSubscription> result =
106                             mDelegate.onSubscribe(message.getString("scope"), appServerKey);
107 
108                     if (result == null) {
109                         callback.sendSuccess(null);
110                         return;
111                     }
112 
113                     result.accept(subscription -> callback.sendSuccess(subscription != null ? subscription.toBundle() : null),
114                         error -> callback.sendSuccess(null));
115                     break;
116                 }
117                 case "GeckoView:PushUnsubscribe": {
118                     final GeckoResult<Void> result = mDelegate.onUnsubscribe(message.getString("scope"));
119                     if (result == null) {
120                         callback.sendSuccess(null);
121                         return;
122                     }
123 
124                     callback.resolveTo(result.map(val -> null));
125                     break;
126                 }
127                 case "GeckoView:PushGetSubscription": {
128                     final GeckoResult<WebPushSubscription> result = mDelegate.onGetSubscription(message.getString("scope"));
129                     if (result == null) {
130                         callback.sendSuccess(null);
131                         return;
132                     }
133 
134                     callback.resolveTo(result.map(subscription ->
135                             subscription != null ? subscription.toBundle() : null));
136                     break;
137                 }
138             }
139         }
140     }
141 }
142