Skip to content

WIP: Customizable per platform hacks #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions reflex-dom/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{ useReflexOptimizer ? false
, filterSource ? (x: x)
}:
self: super:
let
pkgs = self.callPackage ({pkgs}: pkgs) {};
inherit (pkgs) lib stdenv;

reflexOptimizerFlag = lib.optional (useReflexOptimizer && (self.ghc.cross or null) == null) "-fuse-reflex-optimizer";

in pkgs.haskell.lib.overrideCabal
(self.callCabal2nixWithOptions "reflex-dom" (filterSource ./.) (lib.concatStringsSep " " (lib.concatLists [
reflexOptimizerFlag
])) {})
(drv: {
# Hack until https://github.com/NixOS/cabal2nix/pull/432 lands
libraryHaskellDepends = (drv.libraryHaskellDepends or [])
++ stdenv.lib.optionals (with stdenv.hostPlatform; isAndroid && is32bit) [
self.android-activity
] ++ stdenv.lib.optionals (with stdenv.hostPlatform; isWasm && is32bit) [
self.jsaddle-wasm
];
})
192 changes: 109 additions & 83 deletions reflex-dom/java/org/reflexfrp/reflexdom/MainWidget.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
package org.reflexfrp.reflexdom;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.JavascriptInterface;
import android.webkit.MimeTypeMap;
import android.webkit.PermissionRequest;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.graphics.Bitmap;
import java.io.IOException;
import java.io.InputStream;
import android.content.Intent;
import android.content.ActivityNotFoundException;
import java.util.concurrent.atomic.AtomicBoolean;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;

import systems.obsidian.HaskellActivity;

Expand All @@ -50,118 +52,142 @@ private static Object startMainWidget(final HaskellActivity a, String url, long
final AtomicBoolean jsaddleLoaded = new AtomicBoolean(false);

wv.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView _view, String _url) {
Log.i("reflex", "onPageFinished");
boolean alreadyLoaded = jsaddleLoaded.getAndSet(true);
if(!alreadyLoaded) {
Log.i("reflex", "loading jsaddle");
wv.evaluateJavascript(initialJS, null);
}
@Override
public void onPageFinished(WebView _view, String _url) {
Log.i("reflex", "onPageFinished");
boolean alreadyLoaded = jsaddleLoaded.getAndSet(true);
if(!alreadyLoaded) {
Log.i("reflex", "loading jsaddle");
wv.evaluateJavascript(initialJS, null);
}
}

// Re-route / to /android_asset
@Override
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) {
Uri uri = request.getUrl();
if(!uri.getScheme().equals("file"))
return null;

String path = uri.getPath();
path = getAssetPath(path);
// Re-route / to /android_asset
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
Uri uri = request.getUrl();
if(!uri.getScheme().equals("file"))
return null;

String mimeType = getMimeType(uri.toString());
String encoding = "";
String path = uri.getPath();
path = getAssetPath(path);

try {
InputStream data = a.getApplicationContext().getAssets().open(path);
return new WebResourceResponse(mimeType, encoding, data);
}
catch (IOException e) {
Log.i("reflex", "Opening resource failed, Webview will handle the request ..");
e.printStackTrace();
}
String mimeType = getMimeType(uri.toString());
String encoding = "";

return null;
try {
InputStream data = a.getApplicationContext().getAssets().open(path);
return new WebResourceResponse(mimeType, encoding, data);
}
catch (IOException e) {
Log.i("reflex", "Opening resource failed, Webview will handle the request ..");
e.printStackTrace();
}

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if( url != null && !url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file://")) {
try {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
catch(ActivityNotFoundException e) {
Log.e("reflex", "Starting activity for intent '" + url + "' failed!");
}
return true;
} else {
return false;
}
return null;
}

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if(url != null && !url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file://")) {
try {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
catch(ActivityNotFoundException e) {
Log.e("reflex", "Starting activity for intent '" + url + "' failed!");
}
return true;
} else {
return false;
}
}
});

wv.setWebChromeClient(new WebChromeClient() {
// Need to accept permissions to use the camera and audio
@Override
public void onPermissionRequest(final PermissionRequest request) {
if(request.getOrigin().toString().startsWith("file://")) {
a.requestWebViewPermissions(request);
}
else {
a.runOnUiThread(new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
request.deny();
}
});
// Need to accept permissions to use the camera and audio
@Override
public void onPermissionRequest(final PermissionRequest request) {
if(request.getOrigin().toString().startsWith("file://")) {
a.requestWebViewPermissions(request);
}
else {
a.runOnUiThread(new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
request.deny();
}
});
}
}

@Override
public Bitmap getDefaultVideoPoster() {
return Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
}
@Override
public Bitmap getDefaultVideoPoster() {
return Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
}

@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
}

@Override
public boolean onConsoleMessage(ConsoleMessage cm) {
Log.d("JSADDLEJS", String.format("%s @ %d: %s", cm.message(), cm.lineNumber(), cm.sourceId()));
return true;
}

// file upload callback (Android 5.0 (API level 21) -- current)
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE;

@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
a.setFileUploadCallback(filePathCallback);

Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);

if (allowMultiple) {
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}

i.setType("*/*");
a.startActivityForResult(Intent.createChooser(i, "Choose a File"), a.REQUEST_CODE_FILE_PICKER);
return true;
}
});

wv.addJavascriptInterface(new JSaddleCallbacks(jsaddleCallbacks), "jsaddle");

wv.loadUrl(url);

final Handler hnd = new Handler();
return new Object() {
public final void evaluateJavascript(final byte[] js) {
final String jsStr = new String(js, StandardCharsets.UTF_8);
hnd.post(new Runnable() {
@Override
public void run() {
wv.evaluateJavascript(jsStr, null);
}
});
@Override
public void run() {
wv.evaluateJavascript(jsStr, null);
}
});
}
};
}

private static String getMimeType(String url) {
String type = "";
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
return type;
String type = "";
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
return type;
}

/** Get the path of an asset. Strips leading / and leading /android_asset/ */
private static String getAssetPath(String path) {
path = path.startsWith("/android_asset") ? path.substring("/android_asset".length()) : path;
path = path.startsWith("/") ? path.substring(1) : path;
return path;
path = path.startsWith("/android_asset") ? path.substring("/android_asset".length()) : path;
path = path.startsWith("/") ? path.substring(1) : path;
return path;
}

private static class JSaddleCallbacks {
Expand Down
33 changes: 25 additions & 8 deletions reflex-dom/src/Reflex/Dom/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ module Reflex.Dom.Internal
, mainWidget
, mainWidgetWithHead, mainWidgetWithCss, mainWidgetWithHead', mainWidgetInElementById, runApp'
, mainHydrationWidgetWithHead, mainHydrationWidgetWithHead'

#if defined(ANDROID)
, runAndroid
#endif
#if defined(MIN_VERSION_jsaddle_wkwebview)
, runApple
#endif
) where

import Data.ByteString (ByteString)
Expand Down Expand Up @@ -43,21 +50,28 @@ run jsm = do
import Data.Default
import Data.Monoid ((<>))
import Language.Javascript.JSaddle (JSM)
import Language.Javascript.JSaddle.WKWebView (run', mainBundleResourcePath)
import Language.Javascript.JSaddle.WKWebView (AppDelegateConfig, run', mainBundleResourcePath)
import Language.Javascript.JSaddle.WKWebView.Internal (jsaddleMainHTMLWithBaseURL)

-- TODO: upstream to jsaddle-wkwebview
run :: JSM () -> IO ()
run jsm = do
run = runApple def

-- TODO: upstream to jsaddle-wkwebview
runApple :: AppDelegateConfig -> JSM () -> IO ()
runApple cfg jsm = do
let indexHtml = "<!DOCTYPE html><html><head></head><body></body></html>"
baseUrl <- mainBundleResourcePath >>= \case
Nothing -> do
putStrLn "Reflex.Dom.run: unable to find main bundle resource path. Assets may not load properly."
putStrLn "Reflex.Dom.runApple: unable to find main bundle resource path. Assets may not load properly."
return ""
Just p -> return $ "file://" <> p <> "/index.html"
run' def $ jsaddleMainHTMLWithBaseURL indexHtml baseUrl jsm
run' cfg $ jsaddleMainHTMLWithBaseURL indexHtml baseUrl jsm
#else
import Language.Javascript.JSaddle.WKWebView (run)
import Language.Javascript.JSaddle (JSM)
import Language.Javascript.JSaddle.WKWebView (AppDelegateConfig, run, runWithAppConfig)

runApple :: AppDelegateConfig -> JSM () -> IO ()
runApple = runWithAppConfig
#endif
#elif defined(ANDROID)
import Android.HaskellActivity
Expand All @@ -70,10 +84,13 @@ import System.IO
import Language.Javascript.JSaddle (JSM)

run :: JSM () -> IO ()
run jsm = do
run = runAndroid def

runAndroid :: ActivityCallbacks -> JSM () -> IO ()
runAndroid activityCallbacks jsm = do
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
continueWithCallbacks $ def
continueWithCallbacks $ activityCallbacks
{ _activityCallbacks_onCreate = \_ -> do
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably call the passed-in onCreate after we're done with ours.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether that's a good idea, but need to think about the implications of throwing away the passed-in onCreate here.

a <- getHaskellActivity
let startPage = fromString "file:///android_asset/index.html"
Expand Down