[Android] Initial upload support, Context menu for links, More config options

This commit is contained in:
2025-03-18 23:09:21 +01:00
parent 8b47ef18b0
commit 4330c971d1
15 changed files with 161 additions and 59 deletions

View File

@ -27,3 +27,9 @@ android {
compileSdk 34 compileSdk 34
} }
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

View File

@ -2,7 +2,8 @@
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"> android:installLocation="auto"
tools:ignore="UnusedAttribute">
<!-- Lets the app access the Internet — not needed for fully offline apps --> <!-- Lets the app access the Internet — not needed for fully offline apps -->
<!-- <uses-permission android:name="android.permission.INTERNET" /> --> <!-- <uses-permission android:name="android.permission.INTERNET" /> -->

View File

@ -27,7 +27,7 @@
<li><a href="intent://#Intent;action=android.intent.action.VIEW;scheme=http;type=video/mp4;end">intent://</a></li> <li><a href="intent://#Intent;action=android.intent.action.VIEW;scheme=http;type=video/mp4;end">intent://</a></li>
</ul> </ul>
<h3>Files</h3> <h3>Files</h3>
<p>Upload: <label><input type="file"/></label></p> <p>Upload: <label><input type="file" onchange="(function(f){ alert(f.name + ' ' + f.size + ' ' + f.type); })(this.files[0]);" /></label></p>
<p>Download:</p> <p>Download:</p>
<ul> <ul>
<li><a download="Hello World.txt" href="data:text/plain;utf8,Hello World!">data:, .txt</a></li> <li><a download="Hello World.txt" href="data:text/plain;utf8,Hello World!">data:, .txt</a></li>

View File

@ -1,5 +1,7 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android;
import android.webkit.WebSettings;
import org.eu.spacc.spaccdotweb.android.Constants.*; import org.eu.spacc.spaccdotweb.android.Constants.*;
import org.eu.spacc.spaccdotweb.android.helpers.ConfigReader; import org.eu.spacc.spaccdotweb.android.helpers.ConfigReader;
@ -22,6 +24,16 @@ public class Config extends Defaults {
return (value != null ? value : Defaults.ALLOW_STORAGE); return (value != null ? value : Defaults.ALLOW_STORAGE);
} }
public Boolean getAllowZoomControls() {
Boolean value = getBoolean("allow_zoom_controls");
return (value != null ? value : Defaults.ALLOW_ZOOM_CONTROLS);
}
public Boolean getDisplayZoomControls() {
Boolean value = getBoolean("display_zoom_controls");
return (value != null ? value : Defaults.DISPLAY_ZOOM_CONTROLS);
}
public AppIndex getAppIndex() { public AppIndex getAppIndex() {
AppIndex value = (AppIndex)get("app_index"); AppIndex value = (AppIndex)get("app_index");
return (value != null ? value : Defaults.APP_INDEX); return (value != null ? value : Defaults.APP_INDEX);
@ -36,12 +48,18 @@ public class Config extends Defaults {
return getString("remote_index"); return getString("remote_index");
} }
private Object get(String key) { public String getStandardFontFamily() {
if (configReader != null) { return getString("standard_font_family");
return configReader.get(key);
} else {
return null;
} }
public String getUserAgent() {
return getString("user_agent");
}
private Object get(String key) {
return (configReader != null
? configReader.get(key)
: null);
} }
private Boolean getBoolean(String key) { private Boolean getBoolean(String key) {

View File

@ -1,7 +1,7 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android;
public class Constants { public class Constants {
//public enum ActivityCodes { DOWNLOAD_FILE, UPLOAD_FILE } public enum ActivityCodes { UPLOAD_FILE /* , DOWNLOAD_FILE */ }
public enum AppIndex { LOCAL, REMOTE } public enum AppIndex { LOCAL, REMOTE }
public enum DataLocation { INTERNAL, EXTERNAL } public enum DataLocation { INTERNAL, EXTERNAL }
} }

View File

@ -5,6 +5,8 @@ import org.eu.spacc.spaccdotweb.android.Constants.*;
public class Defaults { public class Defaults {
public static final Boolean ALLOW_JAVASCRIPT = true; public static final Boolean ALLOW_JAVASCRIPT = true;
public static final Boolean ALLOW_STORAGE = true; public static final Boolean ALLOW_STORAGE = true;
public static final Boolean ALLOW_ZOOM_CONTROLS = false;
public static final Boolean DISPLAY_ZOOM_CONTROLS = false;
public static final AppIndex APP_INDEX = AppIndex.LOCAL; public static final AppIndex APP_INDEX = AppIndex.LOCAL;
public static final String LOCAL_INDEX = "index.html"; public static final String LOCAL_INDEX = "index.html";
} }

View File

@ -1,23 +1,49 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.webkit.ValueCallback;
import org.eu.spacc.spaccdotweb.android.webview.SpaccWebChromeClient;
import org.eu.spacc.spaccdotweb.android.webview.SpaccWebView; import org.eu.spacc.spaccdotweb.android.webview.SpaccWebView;
import java.io.File; import java.io.File;
public class SpaccWebViewActivity extends Activity { public class SpaccWebViewActivity extends Activity {
protected SpaccWebView webView; protected SpaccWebView webView;
public ValueCallback<Uri> fileUploadCallback;
public ValueCallback<Uri[]> filesUploadCallback;
// @Override @TargetApi(Build.VERSION_CODES.ECLAIR_MR1)
// protected void onActivityResult(int requestCode, int resultCode, Intent data) { @Override
// super.onActivityResult(requestCode, resultCode, data); protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// if (requestCode == Constants.CREATE_FILE_REQUEST_CODE && resultCode == RESULT_OK && data != null) { // if (requestCode == Constants.CREATE_FILE_REQUEST_CODE && resultCode == RESULT_OK && data != null) {
// Uri fileUri = data.getData(); // Uri fileUri = data.getData();
// if (fileUri != null) { // if (fileUri != null) {
// enqueueDownload(Uri.parse(fileUri.toString())); // enqueueDownload(Uri.parse(fileUri.toString()));
// } // }
// } // }
// } if (requestCode == Constants.ActivityCodes.UPLOAD_FILE.ordinal()) {
if (resultCode == Activity.RESULT_OK && data != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && filesUploadCallback != null) {
filesUploadCallback.onReceiveValue(SpaccWebChromeClient.FileChooserParams.parseResult(resultCode, data));
} else if (fileUploadCallback != null) {
fileUploadCallback.onReceiveValue(data.getData());
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && filesUploadCallback != null) {
filesUploadCallback.onReceiveValue(null);
} else if (fileUploadCallback != null) {
fileUploadCallback.onReceiveValue(null);
}
}
fileUploadCallback = null;
filesUploadCallback = null;
}
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {

View File

@ -23,4 +23,15 @@ public class ApiUtils {
context.startActivity(new Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, url.toString())); context.startActivity(new Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, url.toString()));
} }
} }
public static void writeToClipboard(Context context, String text) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(text);
} else {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText(null, text);
clipboard.setPrimaryClip(clip);
}
}
} }

View File

@ -0,0 +1,40 @@
package org.eu.spacc.spaccdotweb.android.webview;
import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import org.eu.spacc.spaccdotweb.android.Constants;
import org.eu.spacc.spaccdotweb.android.SpaccWebViewActivity;
public class SpaccWebChromeClient extends WebChromeClient {
private final SpaccWebViewActivity activity;
public SpaccWebChromeClient(SpaccWebViewActivity activity) {
super();
this.activity = activity;
}
// TODO: Android < 4 support
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams) {
activity.filesUploadCallback = valueCallback;
activity.startActivityForResult(fileChooserParams.createIntent(), Constants.ActivityCodes.UPLOAD_FILE.ordinal());
return true;
}
//@Override // Android 4.1+
protected void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
activity.fileUploadCallback = valueCallback;
activity.startActivityForResult(Intent.createChooser(intent, null), Constants.ActivityCodes.UPLOAD_FILE.ordinal());
}
}

View File

@ -1,32 +1,51 @@
package org.eu.spacc.spaccdotweb.android.webview; package org.eu.spacc.spaccdotweb.android.webview;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.ContextMenu;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import org.eu.spacc.spaccdotweb.android.Config; import org.eu.spacc.spaccdotweb.android.Config;
import org.eu.spacc.spaccdotweb.android.SpaccWebViewActivity;
import org.eu.spacc.spaccdotweb.android.helpers.ConfigReader; import org.eu.spacc.spaccdotweb.android.helpers.ConfigReader;
import org.eu.spacc.spaccdotweb.android.utils.ApiUtils; import org.eu.spacc.spaccdotweb.android.utils.ApiUtils;
public class SpaccWebView extends WebView { public class SpaccWebView extends WebView {
private Config config; private Config config;
private Context context;
@SuppressLint("SetJavaScriptEnabled")
public SpaccWebView(Context context, AttributeSet attrs) { public SpaccWebView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
this.setWebViewClient(new WebViewClient(context)); this.context = context;
this.setWebChromeClient(new WebChromeClient(context)); this.setWebViewClient(new SpaccWebViewClient(context));
this.setWebChromeClient(new SpaccWebChromeClient((SpaccWebViewActivity)context));
this.setDownloadListener(new DownloadListener(context)); this.setDownloadListener(new DownloadListener(context));
this.config = new Config(); this.config = new Config();
this.applyConfig(context); this.applyConfig(context);
} }
// TODO: Implement context menu (long-press on links, images, etc...) // TODO: Implement context menu (long-press on links, images, etc...)
// @Override @Override
// protected void onCreateContextMenu(ContextMenu menu) { protected void onCreateContextMenu(ContextMenu menu) {
// super.onCreateContextMenu(menu); super.onCreateContextMenu(menu);
// } HitTestResult result = getHitTestResult();
switch (result.getType()) {
case HitTestResult.SRC_ANCHOR_TYPE:
case HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
String href = result.getExtra();
menu.setHeaderTitle(href);
menu.add("Copy URL").setOnMenuItemClickListener(menuItem -> {
ApiUtils.writeToClipboard(context, href);
return false;
});
menu.add("Open or Share Externally").setOnMenuItemClickListener(menuItem -> {
ApiUtils.openOrShareUrl(context, Uri.parse(href));
return false;
});
break;
}
}
private void applyConfig(Context context) { private void applyConfig(Context context) {
WebSettings webSettings = this.getSettings(); WebSettings webSettings = this.getSettings();
@ -41,6 +60,12 @@ public class SpaccWebView extends WebView {
} }
ApiUtils.apiRun(3, () -> webSettings.setAllowFileAccess(false)); ApiUtils.apiRun(3, () -> webSettings.setAllowFileAccess(false));
webSettings.setStandardFontFamily(config.getStandardFontFamily());
ApiUtils.apiRun(3, () -> webSettings.setUserAgentString(config.getUserAgent()));
ApiUtils.apiRun(3, () -> webSettings.setBuiltInZoomControls(config.getAllowZoomControls()));
ApiUtils.apiRun(11, () -> webSettings.setDisplayZoomControls(config.getDisplayZoomControls()));
} }
public void loadConfig(Context context, int configResource) { public void loadConfig(Context context, int configResource) {

View File

@ -4,16 +4,18 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.eu.spacc.spaccdotweb.android.utils.ApiUtils; import org.eu.spacc.spaccdotweb.android.utils.ApiUtils;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
public class WebViewClient extends android.webkit.WebViewClient { public class SpaccWebViewClient extends WebViewClient {
private final Context context; private final Context context;
public WebViewClient(Context context) { public SpaccWebViewClient(Context context) {
super(); super();
this.context = context; this.context = context;
} }

View File

@ -1,32 +0,0 @@
package org.eu.spacc.spaccdotweb.android.webview;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.webkit.ValueCallback;
import android.webkit.WebView;
public class WebChromeClient extends android.webkit.WebChromeClient {
private static final int INPUT_FILE_REQUEST_CODE = 1;
private final Context context;
public WebChromeClient(Context context) {
super();
this.context = context;
}
// TODO: This only opens a file selector but then doesn't do anything
// TODO: Android < 5 support
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
contentSelectionIntent.setType("*/*"); // TODO: Read type from HTML input
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
((Activity)context).startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE);
return true;
}
}

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<config> <config>
<boolean name="allow_javascript">true</boolean>
<boolean name="allow_storage">true</boolean>
<AppIndex name="app_index">local</AppIndex> <!-- local or remote --> <AppIndex name="app_index">local</AppIndex> <!-- local or remote -->
<string name="local_index">index.html</string> <string name="local_index">index.html</string>
<string name="remote_index">https://example.com</string> <string name="remote_index">https://example.com</string>
<!-- <string name="standard_font_family"></string> -->
<!-- <string name="user_agent"></string> -->
</config> </config>

View File

@ -1,4 +1,4 @@
plugins { plugins {
id 'com.android.application' version '7.3.1' apply false id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.3.1' apply false id 'com.android.library' version '7.4.2' apply false
} }

View File

@ -5,6 +5,10 @@ pluginManagement {
mavenCentral() mavenCentral()
} }
} }
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {