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.

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