The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

608 lines
21KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. namespace juce
  20. {
  21. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  22. METHOD (constructor, "<init>", "(Landroid/content/Context;)V") \
  23. METHOD (getSettings, "getSettings", "()Landroid/webkit/WebSettings;") \
  24. METHOD (goBack, "goBack", "()V") \
  25. METHOD (goForward, "goForward", "()V") \
  26. METHOD (loadDataWithBaseURL, "loadDataWithBaseURL", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V") \
  27. METHOD (loadUrl, "loadUrl", "(Ljava/lang/String;Ljava/util/Map;)V") \
  28. METHOD (postUrl, "postUrl", "(Ljava/lang/String;[B)V") \
  29. METHOD (reload, "reload", "()V") \
  30. METHOD (setWebChromeClient, "setWebChromeClient", "(Landroid/webkit/WebChromeClient;)V") \
  31. METHOD (setWebViewClient, "setWebViewClient", "(Landroid/webkit/WebViewClient;)V") \
  32. METHOD (stopLoading, "stopLoading", "()V")
  33. DECLARE_JNI_CLASS (AndroidWebView, "android/webkit/WebView")
  34. #undef JNI_CLASS_MEMBERS
  35. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  36. METHOD (constructor, "<init>", "()V")
  37. DECLARE_JNI_CLASS (AndroidWebChromeClient, "android/webkit/WebChromeClient");
  38. #undef JNI_CLASS_MEMBERS
  39. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  40. METHOD (constructor, "<init>", "()V")
  41. DECLARE_JNI_CLASS (AndroidWebViewClient, "android/webkit/WebViewClient");
  42. #undef JNI_CLASS_MEMBERS
  43. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  44. STATICMETHOD (getInstance, "getInstance", "()Landroid/webkit/CookieManager;")
  45. DECLARE_JNI_CLASS (AndroidCookieManager, "android/webkit/CookieManager");
  46. #undef JNI_CLASS_MEMBERS
  47. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  48. METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";J)V")
  49. DECLARE_JNI_CLASS (JuceWebChromeClient, JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceWebChromeClient");
  50. #undef JNI_CLASS_MEMBERS
  51. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  52. METHOD (constructor, "<init>", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH ";J)V") \
  53. METHOD (hostDeleted, "hostDeleted", "()V")
  54. DECLARE_JNI_CLASS (JuceWebViewClient, JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceWebViewClient");
  55. #undef JNI_CLASS_MEMBERS
  56. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  57. METHOD (setBuiltInZoomControls, "setBuiltInZoomControls", "(Z)V") \
  58. METHOD (setDisplayZoomControls, "setDisplayZoomControls", "(Z)V") \
  59. METHOD (setJavaScriptEnabled, "setJavaScriptEnabled", "(Z)V") \
  60. METHOD (setSupportMultipleWindows, "setSupportMultipleWindows", "(Z)V")
  61. DECLARE_JNI_CLASS (WebSettings, "android/webkit/WebSettings");
  62. #undef JNI_CLASS_MEMBERS
  63. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  64. METHOD (toString, "toString", "()Ljava/lang/String;")
  65. DECLARE_JNI_CLASS (SslError, "android/net/http/SslError")
  66. #undef JNI_CLASS_MEMBERS
  67. #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
  68. STATICMETHOD (encode, "encode", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
  69. DECLARE_JNI_CLASS (URLEncoder, "java/net/URLEncoder")
  70. #undef JNI_CLASS_MEMBERS
  71. //==============================================================================
  72. class WebBrowserComponent::Pimpl : public AndroidViewComponent,
  73. public AsyncUpdater
  74. {
  75. public:
  76. Pimpl (WebBrowserComponent& o)
  77. : owner (o)
  78. {
  79. auto* env = getEnv();
  80. setView (env->NewObject (AndroidWebView, AndroidWebView.constructor, android.activity.get()));
  81. auto settings = LocalRef<jobject> (env->CallObjectMethod ((jobject) getView(), AndroidWebView.getSettings));
  82. env->CallVoidMethod (settings, WebSettings.setJavaScriptEnabled, true);
  83. env->CallVoidMethod (settings, WebSettings.setBuiltInZoomControls, true);
  84. env->CallVoidMethod (settings, WebSettings.setDisplayZoomControls, false);
  85. env->CallVoidMethod (settings, WebSettings.setSupportMultipleWindows, true);
  86. juceWebChromeClient = GlobalRef (LocalRef<jobject> (env->NewObject (JuceWebChromeClient, JuceWebChromeClient.constructor,
  87. android.activity.get(),
  88. reinterpret_cast<jlong>(&owner))));
  89. env->CallVoidMethod ((jobject) getView(), AndroidWebView.setWebChromeClient, juceWebChromeClient.get());
  90. juceWebViewClient = GlobalRef (LocalRef<jobject> (env->NewObject (JuceWebViewClient, JuceWebViewClient.constructor,
  91. android.activity.get(),
  92. reinterpret_cast<jlong>(&owner))));
  93. env->CallVoidMethod ((jobject) getView(), AndroidWebView.setWebViewClient, juceWebViewClient.get());
  94. }
  95. ~Pimpl()
  96. {
  97. auto* env = getEnv();
  98. env->CallVoidMethod ((jobject) getView(), AndroidWebView.stopLoading);
  99. auto defaultChromeClient = LocalRef<jobject> (env->NewObject (AndroidWebChromeClient, AndroidWebChromeClient.constructor));
  100. auto defaultViewClient = LocalRef<jobject> (env->NewObject (AndroidWebViewClient, AndroidWebViewClient .constructor));
  101. env->CallVoidMethod ((jobject) getView(), AndroidWebView.setWebChromeClient, defaultChromeClient.get());
  102. env->CallVoidMethod ((jobject) getView(), AndroidWebView.setWebViewClient, defaultViewClient .get());
  103. masterReference.clear();
  104. // if other Java thread is waiting for us to respond to page load request
  105. // wake it up immediately (false answer will be sent), so that it releases
  106. // the lock we need when calling hostDeleted.
  107. responseReadyEvent.signal();
  108. env->CallVoidMethod (juceWebViewClient, JuceWebViewClient.hostDeleted);
  109. }
  110. void goToURL (const String& url,
  111. const StringArray* headers,
  112. const MemoryBlock* postData)
  113. {
  114. auto* env = getEnv();
  115. if (headers == nullptr && postData == nullptr)
  116. {
  117. env->CallVoidMethod ((jobject) getView(), AndroidWebView.loadUrl, javaString (url).get(), 0);
  118. }
  119. else if (headers != nullptr && postData == nullptr)
  120. {
  121. auto headersMap = LocalRef<jobject> (env->NewObject (JavaHashMap,
  122. JavaHashMap.constructorWithCapacity,
  123. headers->size()));
  124. for (const auto& header : *headers)
  125. {
  126. auto name = header.upToFirstOccurrenceOf (":", false, false).trim();
  127. auto value = header.fromFirstOccurrenceOf (":", false, false).trim();
  128. env->CallObjectMethod (headersMap, JavaMap.put,
  129. javaString (name).get(),
  130. javaString (value).get());
  131. }
  132. env->CallVoidMethod ((jobject) getView(), AndroidWebView.loadUrl,
  133. javaString (url).get(), headersMap.get());
  134. }
  135. else if (headers == nullptr && postData != nullptr)
  136. {
  137. auto dataStringJuce = postData->toString();
  138. auto dataStringJava = javaString (dataStringJuce);
  139. auto encodingString = LocalRef<jobject> (env->CallStaticObjectMethod (URLEncoder, URLEncoder.encode,
  140. dataStringJava.get(), javaString ("utf-8").get()));
  141. auto bytes = LocalRef<jbyteArray> ((jbyteArray) env->CallObjectMethod (encodingString, JavaString.getBytes));
  142. env->CallVoidMethod ((jobject) getView(), AndroidWebView.postUrl,
  143. javaString (url).get(), bytes.get());
  144. }
  145. else if (headers != nullptr && postData != nullptr)
  146. {
  147. // There is no support for both extra headers and post data in Android WebView, so
  148. // we need to open URL manually.
  149. URL urlToUse = URL (url).withPOSTData (*postData);
  150. connectionThread.reset (new ConnectionThread (*this, urlToUse, *headers));
  151. }
  152. }
  153. void stop()
  154. {
  155. connectionThread = nullptr;
  156. getEnv()->CallVoidMethod ((jobject) getView(), AndroidWebView.stopLoading);
  157. }
  158. void goBack()
  159. {
  160. connectionThread = nullptr;
  161. getEnv()->CallVoidMethod ((jobject) getView(), AndroidWebView.goBack);
  162. }
  163. void goForward()
  164. {
  165. connectionThread = nullptr;
  166. getEnv()->CallVoidMethod ((jobject) getView(), AndroidWebView.goForward);
  167. }
  168. void refresh()
  169. {
  170. connectionThread = nullptr;
  171. getEnv()->CallVoidMethod ((jobject) getView(), AndroidWebView.reload);
  172. }
  173. void handleAsyncUpdate()
  174. {
  175. jassert (connectionThread != nullptr);
  176. if (connectionThread == nullptr)
  177. return;
  178. auto& result = connectionThread->getResult();
  179. if (result.statusCode >= 200 && result.statusCode < 300)
  180. {
  181. auto url = javaString (result.url);
  182. auto data = javaString (result.data);
  183. auto mimeType = javaString ("text/html");
  184. auto encoding = javaString ("utf-8");
  185. getEnv()->CallVoidMethod ((jobject) getView(), AndroidWebView.loadDataWithBaseURL,
  186. url.get(), data.get(), mimeType.get(),
  187. encoding.get(), 0);
  188. }
  189. else
  190. {
  191. owner.pageLoadHadNetworkError (result.description);
  192. }
  193. }
  194. bool handlePageAboutToLoad (const String& url)
  195. {
  196. if (MessageManager::getInstance()->isThisTheMessageThread())
  197. return owner.pageAboutToLoad (url);
  198. WeakReference<Pimpl> weakRef (this);
  199. if (weakRef == nullptr)
  200. return false;
  201. responseReadyEvent.reset();
  202. bool shouldLoad = false;
  203. MessageManager::callAsync ([weakRef, url, &shouldLoad]
  204. {
  205. if (weakRef == nullptr)
  206. return;
  207. shouldLoad = weakRef->owner.pageAboutToLoad (url);
  208. weakRef->responseReadyEvent.signal();
  209. });
  210. responseReadyEvent.wait (-1);
  211. return shouldLoad;
  212. }
  213. private:
  214. class ConnectionThread : private Thread
  215. {
  216. public:
  217. struct Result
  218. {
  219. String url;
  220. int statusCode = 0;
  221. String description;
  222. String data;
  223. };
  224. ConnectionThread (Pimpl& ownerToUse,
  225. URL& url,
  226. const StringArray& headers)
  227. : Thread ("WebBrowserComponent::Pimpl::ConnectionThread"),
  228. owner (ownerToUse),
  229. webInputStream (new WebInputStream (url, true))
  230. {
  231. webInputStream->withExtraHeaders (headers.joinIntoString ("\n"));
  232. webInputStream->withConnectionTimeout (10000);
  233. result.url = url.toString (true);
  234. startThread();
  235. }
  236. ~ConnectionThread()
  237. {
  238. webInputStream->cancel();
  239. signalThreadShouldExit();
  240. waitForThreadToExit (10000);
  241. webInputStream = nullptr;
  242. }
  243. void run() override
  244. {
  245. if (! webInputStream->connect (nullptr))
  246. {
  247. result.description = "Could not establish connection";
  248. owner.triggerAsyncUpdate();
  249. return;
  250. }
  251. result.statusCode = webInputStream->getStatusCode();
  252. result.description = "Status code: " + String (result.statusCode);
  253. readFromInputStream();
  254. owner.triggerAsyncUpdate();
  255. }
  256. const Result& getResult() { return result; }
  257. private:
  258. void readFromInputStream()
  259. {
  260. MemoryOutputStream ostream;
  261. while (true)
  262. {
  263. if (threadShouldExit())
  264. return;
  265. char buffer [8192];
  266. const int num = webInputStream->read (buffer, sizeof (buffer));
  267. if (num <= 0)
  268. break;
  269. ostream.write (buffer, (size_t) num);
  270. }
  271. result.data = ostream.toUTF8();
  272. }
  273. Pimpl& owner;
  274. std::unique_ptr<WebInputStream> webInputStream;
  275. Result result;
  276. };
  277. WebBrowserComponent& owner;
  278. GlobalRef juceWebChromeClient;
  279. GlobalRef juceWebViewClient;
  280. std::unique_ptr<ConnectionThread> connectionThread;
  281. WaitableEvent responseReadyEvent;
  282. WeakReference<Pimpl>::Master masterReference;
  283. friend class WeakReference<Pimpl>;
  284. };
  285. //==============================================================================
  286. WebBrowserComponent::WebBrowserComponent (const bool unloadWhenHidden)
  287. : blankPageShown (false),
  288. unloadPageWhenBrowserIsHidden (unloadWhenHidden)
  289. {
  290. setOpaque (true);
  291. browser.reset (new Pimpl (*this));
  292. addAndMakeVisible (browser.get());
  293. }
  294. WebBrowserComponent::~WebBrowserComponent()
  295. {
  296. }
  297. //==============================================================================
  298. void WebBrowserComponent::goToURL (const String& url,
  299. const StringArray* headers,
  300. const MemoryBlock* postData)
  301. {
  302. lastURL = url;
  303. if (headers != nullptr)
  304. lastHeaders = *headers;
  305. else
  306. lastHeaders.clear();
  307. if (postData != nullptr)
  308. lastPostData = *postData;
  309. else
  310. lastPostData.reset();
  311. blankPageShown = false;
  312. browser->goToURL (url, headers, postData);
  313. }
  314. void WebBrowserComponent::stop()
  315. {
  316. browser->stop();
  317. }
  318. void WebBrowserComponent::goBack()
  319. {
  320. lastURL.clear();
  321. blankPageShown = false;
  322. browser->goBack();
  323. }
  324. void WebBrowserComponent::goForward()
  325. {
  326. lastURL.clear();
  327. browser->goForward();
  328. }
  329. void WebBrowserComponent::refresh()
  330. {
  331. browser->refresh();
  332. }
  333. //==============================================================================
  334. void WebBrowserComponent::paint (Graphics& g)
  335. {
  336. g.fillAll (Colours::white);
  337. }
  338. void WebBrowserComponent::checkWindowAssociation()
  339. {
  340. if (isShowing())
  341. {
  342. if (blankPageShown)
  343. goBack();
  344. }
  345. else
  346. {
  347. if (unloadPageWhenBrowserIsHidden && ! blankPageShown)
  348. {
  349. // when the component becomes invisible, some stuff like flash
  350. // carries on playing audio, so we need to force it onto a blank
  351. // page to avoid this, (and send it back when it's made visible again).
  352. blankPageShown = true;
  353. browser->goToURL ("about:blank", 0, 0);
  354. }
  355. }
  356. }
  357. void WebBrowserComponent::reloadLastURL()
  358. {
  359. if (lastURL.isNotEmpty())
  360. {
  361. goToURL (lastURL, &lastHeaders, lastPostData.getSize() == 0 ? nullptr : &lastPostData);
  362. lastURL.clear();
  363. }
  364. }
  365. void WebBrowserComponent::parentHierarchyChanged()
  366. {
  367. checkWindowAssociation();
  368. }
  369. void WebBrowserComponent::resized()
  370. {
  371. browser->setSize (getWidth(), getHeight());
  372. }
  373. void WebBrowserComponent::visibilityChanged()
  374. {
  375. checkWindowAssociation();
  376. }
  377. void WebBrowserComponent::focusGained (FocusChangeType)
  378. {
  379. }
  380. void WebBrowserComponent::clearCookies()
  381. {
  382. auto* env = getEnv();
  383. auto cookieManager = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidCookieManager,
  384. AndroidCookieManager.getInstance));
  385. const bool apiAtLeast21 = env->CallStaticIntMethod (JuceAppActivity, JuceAppActivity.getAndroidSDKVersion) >= 21;
  386. jmethodID clearCookiesMethod = 0;
  387. if (apiAtLeast21)
  388. {
  389. clearCookiesMethod = env->GetMethodID (AndroidCookieManager, "removeAllCookies", "(Landroid/webkit/ValueCallback;)V");
  390. env->CallVoidMethod (cookieManager, clearCookiesMethod, 0);
  391. }
  392. else
  393. {
  394. clearCookiesMethod = env->GetMethodID (AndroidCookieManager, "removeAllCookie", "()V");
  395. env->CallVoidMethod (cookieManager, clearCookiesMethod);
  396. }
  397. }
  398. JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, webViewPageLoadStarted, bool, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*webView*/, jobject url))
  399. {
  400. setEnv (env);
  401. return juce_webViewPageLoadStarted (reinterpret_cast<WebBrowserComponent*> (host),
  402. juceString (static_cast<jstring> (url)));
  403. }
  404. bool juce_webViewPageLoadStarted (WebBrowserComponent* browserComponent, const String& url)
  405. {
  406. return browserComponent->browser->handlePageAboutToLoad (url);
  407. }
  408. JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, webViewPageLoadFinished, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*webView*/, jobject url))
  409. {
  410. setEnv (env);
  411. reinterpret_cast<WebBrowserComponent*> (host)->pageFinishedLoading (juceString (static_cast<jstring> (url)));
  412. }
  413. JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, webViewReceivedError, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*webView*/, jobject /*request*/, jobject error))
  414. {
  415. setEnv (env);
  416. jclass errorClass = env->FindClass ("android/webkit/WebResourceError");
  417. if (errorClass != 0)
  418. {
  419. jmethodID method = env->GetMethodID (errorClass, "getDescription", "()Ljava/lang/CharSequence;");
  420. if (method != 0)
  421. {
  422. auto sequence = LocalRef<jobject> (env->CallObjectMethod (error, method));
  423. auto errorString = LocalRef<jstring> ((jstring) env->CallObjectMethod (sequence, JavaCharSequence.toString));
  424. reinterpret_cast<WebBrowserComponent*> (host)->pageLoadHadNetworkError (juceString (errorString));
  425. return;
  426. }
  427. }
  428. // Should never get here!
  429. jassertfalse;
  430. reinterpret_cast<WebBrowserComponent*> (host)->pageLoadHadNetworkError ({});
  431. }
  432. JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, webViewReceivedHttpError, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*webView*/, jobject /*request*/, jobject errorResponse))
  433. {
  434. setEnv (env);
  435. jclass responseClass = env->FindClass ("android/webkit/WebResourceResponse");
  436. if (responseClass != 0)
  437. {
  438. jmethodID method = env->GetMethodID (responseClass, "getReasonPhrase", "()Ljava/lang/String;");
  439. if (method != 0)
  440. {
  441. auto errorString = LocalRef<jstring> ((jstring) env->CallObjectMethod (errorResponse, method));
  442. reinterpret_cast<WebBrowserComponent*> (host)->pageLoadHadNetworkError (juceString (errorString));
  443. return;
  444. }
  445. }
  446. // Should never get here!
  447. jassertfalse;
  448. reinterpret_cast<WebBrowserComponent*> (host)->pageLoadHadNetworkError ({});
  449. }
  450. JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, webViewReceivedSslError, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*webView*/, jobject /*sslErrorHandler*/, jobject sslError))
  451. {
  452. setEnv (env);
  453. auto errorString = LocalRef<jstring> ((jstring) env->CallObjectMethod (sslError, SslError.toString));
  454. reinterpret_cast<WebBrowserComponent*> (host)->pageLoadHadNetworkError (juceString (errorString));
  455. }
  456. JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, webViewCloseWindowRequest, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*webView*/))
  457. {
  458. setEnv (env);
  459. reinterpret_cast<WebBrowserComponent*> (host)->windowCloseRequest();
  460. }
  461. JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, webViewCreateWindowRequest, void, (JNIEnv* env, jobject /*activity*/, jlong host, jobject /*webView*/))
  462. {
  463. setEnv (env);
  464. reinterpret_cast<WebBrowserComponent*> (host)->newWindowAttemptingToLoad ({});
  465. }
  466. } // namespace juce