1 /*
2  * ShinyApplicationPresenter.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.shiny;
16 
17 import org.rstudio.core.client.BrowseCap;
18 import org.rstudio.core.client.StringUtil;
19 import org.rstudio.core.client.command.CommandBinder;
20 import org.rstudio.core.client.command.Handler;
21 import org.rstudio.studio.client.application.events.EventBus;
22 import org.rstudio.studio.client.common.GlobalDisplay;
23 import org.rstudio.studio.client.common.satellite.Satellite;
24 import org.rstudio.studio.client.shiny.model.ShinyApplicationParams;
25 import org.rstudio.studio.client.shiny.events.ShinyApplicationStatusEvent;
26 import org.rstudio.studio.client.workbench.commands.Commands;
27 import org.rstudio.studio.client.workbench.model.Session;
28 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
29 
30 import com.google.gwt.core.client.JavaScriptObject;
31 import com.google.gwt.event.dom.client.LoadHandler;
32 import com.google.gwt.user.client.ui.IsWidget;
33 import com.google.gwt.user.client.ui.Widget;
34 import com.google.inject.Inject;
35 
36 public class ShinyApplicationPresenter implements
37       IsWidget,
38       ShinyApplicationStatusEvent.Handler,
39       ShinyDisconnectNotifier.ShinyDisconnectSource
40 {
41    public interface Binder
42           extends CommandBinder<Commands, ShinyApplicationPresenter>
43    {}
44 
45    public interface Display extends IsWidget
46    {
getDocumentTitle()47       String getDocumentTitle();
getUrl()48       String getUrl();
getAbsoluteUrl()49       String getAbsoluteUrl();
showApp(ShinyApplicationParams params, LoadHandler handler)50       void showApp(ShinyApplicationParams params, LoadHandler handler);
reloadApp()51       void reloadApp();
52    }
53 
54    @Inject
ShinyApplicationPresenter(Display view, GlobalDisplay globalDisplay, Binder binder, final Commands commands, EventBus eventBus, Satellite satellite, Session session, UserPrefs prefs)55    public ShinyApplicationPresenter(Display view,
56                                GlobalDisplay globalDisplay,
57                                Binder binder,
58                                final Commands commands,
59                                EventBus eventBus,
60                                Satellite satellite,
61                                Session session,
62                                UserPrefs prefs)
63    {
64       view_ = view;
65       satellite_ = satellite;
66       events_ = eventBus;
67       globalDisplay_ = globalDisplay;
68       disconnect_ = new ShinyDisconnectNotifier(this);
69       session_ = session;
70       prefs_ = prefs;
71 
72       loadHandler_ = (evt) ->
73       {
74          if (BrowseCap.isFirefox())
75          {
76             disconnect_.unsuppress();
77          }
78       };
79 
80       binder.bind(commands, this);
81 
82       initializeEvents();
83    }
84 
85    @Override
asWidget()86    public Widget asWidget()
87    {
88       return view_.asWidget();
89    }
90 
91    @Override
onShinyApplicationStatus(ShinyApplicationStatusEvent event)92    public void onShinyApplicationStatus(ShinyApplicationStatusEvent event)
93    {
94       if (event.getParams().getState() == ShinyApplicationParams.STATE_RELOADING)
95       {
96          reload();
97       }
98    }
99 
100    @Override
getShinyUrl()101    public String getShinyUrl()
102    {
103       return view_.getAbsoluteUrl();
104    }
105 
106    @Override
onShinyDisconnect()107    public void onShinyDisconnect()
108    {
109       appStopped_ = true;
110       notifyShinyAppDisconnected(params_);
111       closeShinyApp();
112    }
113 
114    @Handler
onReloadShinyApp()115    public void onReloadShinyApp()
116    {
117       reload();
118    }
119 
120    @Handler
onViewerPopout()121    public void onViewerPopout()
122    {
123       globalDisplay_.openWindow(params_.getUrl());
124    }
125 
loadApp(ShinyApplicationParams params)126    public void loadApp(ShinyApplicationParams params)
127    {
128       params_ = params;
129       view_.showApp(params, loadHandler_);
130    }
131 
initializeEvents()132    private native void initializeEvents() /*-{
133       var thiz = this;
134 
135       // we observed that sometimes (with RStudio Server) the 'unload' event was
136       // not fired on window closing, and yet 'beforeunload' was not fired with
137       // RStudio Desktop. to be safe, attach to both events and just properly handle
138       // the close request there
139       $wnd.addEventListener(
140             "unload",
141             $entry(function() {
142                thiz.@org.rstudio.studio.client.shiny.ShinyApplicationPresenter::onClose()();
143             }),
144             true);
145 
146       $wnd.addEventListener(
147             "beforeunload",
148             $entry(function() {
149                thiz.@org.rstudio.studio.client.shiny.ShinyApplicationPresenter::onClose()();
150             }),
151             true);
152    }-*/;
153 
onClose()154    private void onClose()
155    {
156       // don't stop the app if the window is closing just to be opened again.
157       // (we close and reopen as a workaround to forcefully activate the window
158       // on browsers that don't permit manual event reactivation)
159       if (satellite_.isReactivatePending())
160          return;
161 
162       if (closed_)
163          return;
164 
165       closed_ = true;
166 
167       ShinyApplicationParams params = ShinyApplicationParams.create(
168             params_.getPath(),
169             ShinyApplicationSatellite.getIdFromName(
170                   satellite_.getSatelliteName()),
171             params_.getUrl(),
172             appStopped_ ?
173                ShinyApplicationParams.STATE_STOPPED :
174                ShinyApplicationParams.STATE_STOPPING);
175       notifyShinyAppClosed(params);
176    }
177 
reload()178    private void reload()
179    {
180       if (BrowseCap.isFirefox() && !StringUtil.isNullOrEmpty(getShinyUrl()))
181       {
182          // Firefox allows Shiny's disconnection notification (a "disconnected"
183          // postmessage) through during the unload that occurs during refresh.
184          // To keep this transient disconnection from being treated as an app
185          // stop, we temporarily suppress it here.
186          disconnect_.suppress();
187       }
188       view_.reloadApp();
189    }
190 
closeShinyApp()191    private final native void closeShinyApp() /*-{
192       $wnd.close();
193    }-*/;
194 
notifyShinyAppClosed(JavaScriptObject params)195    private final native void notifyShinyAppClosed(JavaScriptObject params) /*-{
196       $wnd.opener.notifyShinyAppClosed(params);
197    }-*/;
198 
notifyShinyAppDisconnected(JavaScriptObject params)199    private final native void notifyShinyAppDisconnected(JavaScriptObject params) /*-{
200       if ($wnd.opener)
201          $wnd.opener.notifyShinyAppDisconnected(params);
202    }-*/;
203 
204    private final Display view_;
205    private final Satellite satellite_;
206    private final EventBus events_;
207    private final GlobalDisplay globalDisplay_;
208    private final ShinyDisconnectNotifier disconnect_;
209    private final Session session_;
210    private final UserPrefs prefs_;
211    private final LoadHandler loadHandler_;
212 
213    private ShinyApplicationParams params_;
214    private boolean closed_ = false;
215    private boolean appStopped_ = false;
216    private boolean popoutToBrowser_ = false;
217 }
218