From 6313d99e3053b77a9e9e559b8f404eb92f6491a8 Mon Sep 17 00:00:00 2001 From: octospacc Date: Sun, 25 May 2025 00:10:05 +0200 Subject: [PATCH] Update SpaccWebView --- SpaccDotWeb.Android/.gitignore | 2 + SpaccDotWeb.Android/app/build.gradle | 26 +++- .../app/src/main/AndroidManifest.xml | 26 +++- .../app/src/main/assets/index.html | 1 + .../spaccwebviewapplication/MainActivity.java | 125 ++++++++++++++++++ .../eu/spacc/spaccdotweb/android/Config.java | 26 +++- .../spacc/spaccdotweb/android/Defaults.java | 4 + .../android/SpaccWebViewActivity.java | 3 +- .../android/helpers/ConfigReader.java | 3 +- .../android/helpers/DataMoveHelper.java | 43 +++--- .../android/helpers/SharedPrefHelper.java | 39 +++++- .../spaccdotweb/android/utils/ApiUtils.java | 17 ++- .../spaccdotweb/android/utils/FileUtils.java | 19 +++ .../android/utils/StorageUtils.java | 1 + .../android/webview/DownloadListener.java | 1 + .../android/webview/SpaccWebChromeClient.java | 36 +++++ .../android/webview/SpaccWebView.java | 117 ++++++++++++++-- .../android/webview/SpaccWebViewClient.java | 31 ++++- .../app/src/main/res/layout/activity_main.xml | 1 + .../app/src/main/res/menu/main.xml | 13 ++ .../app/src/main/res/values-it/strings.xml | 40 +++++- .../app/src/main/res/values/strings.xml | 43 +++++- .../app/src/main/res/xml/app_config.xml | 15 ++- SpaccDotWeb.Android/settings.gradle | 2 + 24 files changed, 560 insertions(+), 74 deletions(-) create mode 100644 SpaccDotWeb.Android/app/src/main/res/menu/main.xml diff --git a/SpaccDotWeb.Android/.gitignore b/SpaccDotWeb.Android/.gitignore index 878c2cd..5d172f4 100644 --- a/SpaccDotWeb.Android/.gitignore +++ b/SpaccDotWeb.Android/.gitignore @@ -1,6 +1,7 @@ *.iml .gradle /local.properties +/keystore.properties .idea .DS_Store /build @@ -8,3 +9,4 @@ .externalNativeBuild .cxx local.properties +keystore.properties diff --git a/SpaccDotWeb.Android/app/build.gradle b/SpaccDotWeb.Android/app/build.gradle index ef53b7f..217bba2 100644 --- a/SpaccDotWeb.Android/app/build.gradle +++ b/SpaccDotWeb.Android/app/build.gradle @@ -3,29 +3,45 @@ plugins { } android { - // Adjust the very following based on the application to ship + // Adjust namespace and defaultConfig very following based on the application to ship + namespace 'com.example.spaccwebviewapplication' + defaultConfig { applicationId 'com.example.spaccwebviewapplication' versionCode 1 versionName '1.0' - minSdk 1 + //noinspection OldTargetApi targetSdk 34 + compileSdk 34 + } + + signingConfigs { + release { + def Properties localProps = new Properties() + def Properties keyProps = new Properties() + localProps.load(new FileInputStream(file('../local.properties'))) + keyProps.load(new FileInputStream(file('../keystore.properties'))) + storeFile file(localProps['storeFile']) + storePassword keyProps['storePassword'] + keyPassword keyProps['keyPassword'] + keyAlias keyProps['keyAlias'] + } } buildTypes { debug { applicationIdSuffix '.dbg' + versionNameSuffix '.debug' + signingConfig signingConfigs.debug } release { minifyEnabled true shrinkResources true - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } } - - compileSdk 34 } java { diff --git a/SpaccDotWeb.Android/app/src/main/AndroidManifest.xml b/SpaccDotWeb.Android/app/src/main/AndroidManifest.xml index 64fd597..57efdbd 100644 --- a/SpaccDotWeb.Android/app/src/main/AndroidManifest.xml +++ b/SpaccDotWeb.Android/app/src/main/AndroidManifest.xml @@ -6,44 +6,56 @@ tools:ignore="UnusedAttribute"> - + - - + + - + + + + + + + + + - + diff --git a/SpaccDotWeb.Android/app/src/main/assets/index.html b/SpaccDotWeb.Android/app/src/main/assets/index.html index 8e30cce..38fb0a0 100644 --- a/SpaccDotWeb.Android/app/src/main/assets/index.html +++ b/SpaccDotWeb.Android/app/src/main/assets/index.html @@ -4,6 +4,7 @@

SpaccWebView Example Android Application

Repository: https://gitlab.com/SpaccInc/SpaccDotWeb.

Tests

+Cat!

JavaScript

diff --git a/SpaccDotWeb.Android/app/src/main/java/com/example/spaccwebviewapplication/MainActivity.java b/SpaccDotWeb.Android/app/src/main/java/com/example/spaccwebviewapplication/MainActivity.java index 9949ef4..502e43f 100644 --- a/SpaccDotWeb.Android/app/src/main/java/com/example/spaccwebviewapplication/MainActivity.java +++ b/SpaccDotWeb.Android/app/src/main/java/com/example/spaccwebviewapplication/MainActivity.java @@ -1,20 +1,145 @@ package com.example.spaccwebviewapplication; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.webkit.WebView; +import android.widget.EditText; + +import org.eu.spacc.spaccdotweb.android.utils.ApiUtils; import org.eu.spacc.spaccdotweb.android.helpers.DataMoveHelper; import org.eu.spacc.spaccdotweb.android.SpaccWebViewActivity; +import org.eu.spacc.spaccdotweb.android.webview.SpaccWebViewClient; public class MainActivity extends SpaccWebViewActivity { + private ActionBar actionBar = null; + private Menu menu = null; + private long pageStartTime = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + ApiUtils.apiRun(11, () -> this.actionBar = getActionBar()); + DataMoveHelper.run(this, R.string.exit, R.string.move_app_data, R.string.move_app_data_info); this.webView = findViewById(R.id.webview); + this.webView.setStrings(R.string.open_menu, R.string.open_externally_menu, R.string.copy_url_menu); + this.webView.setWebViewClient(new SpaccWebViewClient(this) { + @SuppressLint("UseRequiresApi") + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + pageStartTime = System.currentTimeMillis(); + if (menu != null) { + menu.findItem(R.id.stop).setVisible(true); + menu.findItem(R.id.reload).setVisible(false); + menu.findItem(R.id.open_externally).setVisible(!ApiUtils.isInternalUrl(Uri.parse(url))); + menu.findItem(R.id.about_app).setVisible(webView.getConfig().getAboutPage() != null); + menu.findItem(R.id.backwards).setEnabled(webView.canGoBack()); + menu.findItem(R.id.forward).setEnabled(webView.canGoForward()); + } + if (actionBar != null) { + actionBar.setSubtitle(R.string.loading); + } +// if (justStarted) { +// new Handler().postDelayed(() -> Objects.requireNonNull(getActionBar()).hide(), 3000); +// justStarted = false; +// } + } + + @SuppressLint("UseRequiresApi") + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if (menu != null) { + menu.findItem(R.id.stop).setVisible(false); + menu.findItem(R.id.reload).setVisible(true); + } + if (actionBar != null && pageStartTime != 0) { + actionBar.setSubtitle("~" + (System.currentTimeMillis() - pageStartTime) + "ms"); + } + } + }); this.webView.loadConfig(this, R.xml.app_config); this.webView.loadAppIndex(); } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + if (actionBar == null) { + menu.findItem(R.id.hide).setVisible(false); + } + return super.onCreateOptionsMenu(this.menu = menu); + } + + @SuppressLint({"NonConstantResourceId", "UseRequiresApi"}) + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.stop: + this.webView.stopLoading(); + break; + case R.id.reload: + this.webView.reload(); + break; + case R.id.backwards: + this.webView.goBack(); + break; + case R.id.forward: + this.webView.goForward(); + break; + case R.id.copy_url: + ApiUtils.writeToClipboard(this, webView.getUrl()); + break; + case R.id.open_externally: + ApiUtils.openOrShareUrl(this, Uri.parse(webView.getUrl())); + break; + case R.id.exec_script: + EditText scriptText = new EditText(this); + new AlertDialog.Builder(this) + .setTitle(R.string.execute_javascript) + .setView(scriptText) + .setPositiveButton("OK", (dialogInterface, i) -> webView.injectScript(scriptText.getText().toString())) + .setNeutralButton(R.string.cancel, null) + .show(); + break; + case R.id.hide: + actionBar.hide(); + break; + case R.id.exit: + finish(); + break; + case R.id.about_app: + ApiUtils.openOrShareUrl(this, Uri.parse(webView.getConfig().getAboutPage())); + break; + } + return super.onOptionsItemSelected(item); + } + + @SuppressLint("UseRequiresApi") + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void onBackPressed() { + if (actionBar == null) { + super.onBackPressed(); + } else if (actionBar.isShowing()) { + actionBar.hide(); + } else { + actionBar.show(); + } + } } diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Config.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Config.java index e150f73..36e13ed 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Config.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Config.java @@ -1,7 +1,5 @@ package org.eu.spacc.spaccdotweb.android; -import android.webkit.WebSettings; - import org.eu.spacc.spaccdotweb.android.Constants.*; import org.eu.spacc.spaccdotweb.android.helpers.ConfigReader; @@ -14,6 +12,10 @@ public class Config extends Defaults { this.configReader = configReader; } + public String getAboutPage() { + return getString("about_page"); + } + public Boolean getAllowJavascript() { Boolean value = getBoolean("allow_javascript"); return (value != null ? value : Defaults.ALLOW_JAVASCRIPT); @@ -24,6 +26,26 @@ public class Config extends Defaults { return (value != null ? value : Defaults.ALLOW_STORAGE); } + public Boolean getAllowAutoplay() { + Boolean value = getBoolean("allow_autoplay"); + return (value != null ? value : Defaults.ALLOW_AUTOPLAY); + } + + public Boolean getAllowDrmMedia() { + Boolean value = getBoolean("allow_drm_media"); + return (value != null ? value : Defaults.ALLOW_DRM_MEDIA); + } + + public Boolean getAllowAudioCapture() { + Boolean value = getBoolean("allow_audio_capture"); + return (value != null ? value : Defaults.ALLOW_AUDIO_CAPTURE); + } + + public Boolean getAllowVideoCapture() { + Boolean value = getBoolean("allow_video_capture"); + return (value != null ? value : Defaults.ALLOW_VIDEO_CAPTURE); + } + public Boolean getAllowZoomControls() { Boolean value = getBoolean("allow_zoom_controls"); return (value != null ? value : Defaults.ALLOW_ZOOM_CONTROLS); diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Defaults.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Defaults.java index b3be7cf..6f1ec75 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Defaults.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/Defaults.java @@ -5,6 +5,10 @@ import org.eu.spacc.spaccdotweb.android.Constants.*; public class Defaults { public static final Boolean ALLOW_JAVASCRIPT = true; public static final Boolean ALLOW_STORAGE = true; + public static final Boolean ALLOW_AUTOPLAY = true; + public static final Boolean ALLOW_DRM_MEDIA = true; + public static final Boolean ALLOW_AUDIO_CAPTURE = false; + public static final Boolean ALLOW_VIDEO_CAPTURE = false; public static final Boolean ALLOW_ZOOM_CONTROLS = false; public static final Boolean DISPLAY_ZOOM_CONTROLS = false; public static final AppIndex APP_INDEX = AppIndex.LOCAL; diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/SpaccWebViewActivity.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/SpaccWebViewActivity.java index 0d3a056..dbe7caf 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/SpaccWebViewActivity.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/SpaccWebViewActivity.java @@ -7,9 +7,10 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.webkit.ValueCallback; +import java.io.File; + import org.eu.spacc.spaccdotweb.android.webview.SpaccWebChromeClient; import org.eu.spacc.spaccdotweb.android.webview.SpaccWebView; -import java.io.File; public class SpaccWebViewActivity extends Activity { protected SpaccWebView webView; diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/ConfigReader.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/ConfigReader.java index adfdd88..af8b86a 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/ConfigReader.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/ConfigReader.java @@ -5,9 +5,10 @@ import android.content.res.XmlResourceParser; import java.io.IOException; import java.util.HashMap; import java.util.Map; -import org.eu.spacc.spaccdotweb.android.Constants.*; import org.xmlpull.v1.XmlPullParserException; +import org.eu.spacc.spaccdotweb.android.Constants.*; + public class ConfigReader { private final Map configData = new HashMap<>(); diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/DataMoveHelper.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/DataMoveHelper.java index 2780f55..0a56566 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/DataMoveHelper.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/DataMoveHelper.java @@ -3,18 +3,17 @@ package org.eu.spacc.spaccdotweb.android.helpers; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.os.Build; +import java.io.IOException; + import org.eu.spacc.spaccdotweb.android.Constants; import org.eu.spacc.spaccdotweb.android.utils.StorageUtils; import org.eu.spacc.spaccdotweb.android.utils.FileUtils; -import java.io.IOException; public class DataMoveHelper { public static void run(Context context, int labelExit, int dialogTitle, int dialogMessage) { - Activity activity = (Activity)context; SharedPrefHelper sharedPrefHelper = new SharedPrefHelper(context); Constants.DataLocation dataLocationReal = (StorageUtils.isInstalledOnExternalStorage(context) ? Constants.DataLocation.EXTERNAL : Constants.DataLocation.INTERNAL); Integer dataLocationSaved = sharedPrefHelper.getInt("data_location"); @@ -22,29 +21,21 @@ public class DataMoveHelper { sharedPrefHelper.setInt("data_location", dataLocationReal.ordinal()); } else if (!dataLocationSaved.equals(dataLocationReal.ordinal())) { new AlertDialog.Builder(context) - .setTitle(dialogTitle) - .setMessage(dialogMessage) - .setCancelable(false) - .setNegativeButton(labelExit, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - ((Activity)context).finish(); - } - }) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - // TODO: Check that the storage locations are all present to copy, and implement an error dialog - try { - FileUtils.moveDirectory(StorageUtils.dataDirFromEnum(context, Constants.DataLocation.values()[dataLocationSaved]), StorageUtils.dataDirFromEnum(context, dataLocationReal), false); - } catch (IOException e) { - throw new RuntimeException(e); - } - sharedPrefHelper.setInt("data_location", dataLocationReal.ordinal()); - restartActivity(context); - } - }) - .show(); + .setTitle(dialogTitle) + .setMessage(dialogMessage) + .setCancelable(false) + .setNegativeButton(labelExit, (dialogInterface, i) -> ((Activity)context).finish()) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + // TODO: Check that the storage locations are all present to copy, and implement an error dialog + try { + FileUtils.moveDirectory(StorageUtils.dataDirFromEnum(context, Constants.DataLocation.values()[dataLocationSaved]), StorageUtils.dataDirFromEnum(context, dataLocationReal), false); + } catch (IOException e) { + throw new RuntimeException(e); + } + sharedPrefHelper.setInt("data_location", dataLocationReal.ordinal()); + restartActivity(context); + }) + .show(); } } diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/SharedPrefHelper.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/SharedPrefHelper.java index 658ac43..7959d42 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/SharedPrefHelper.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/helpers/SharedPrefHelper.java @@ -3,6 +3,9 @@ package org.eu.spacc.spaccdotweb.android.helpers; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; +import org.json.JSONArray; +import org.json.JSONException; +import java.util.ArrayList; public class SharedPrefHelper { private final SharedPreferences sharedPref; @@ -11,8 +14,16 @@ public class SharedPrefHelper { this.sharedPref = context.getSharedPreferences("SpaccWebView", Context.MODE_PRIVATE); } + public SharedPrefHelper(Context context, String name) { + this.sharedPref = context.getSharedPreferences(name, Context.MODE_PRIVATE); + } + public Integer getInt(String name) { - Integer value = (Integer)sharedPref.getInt(name, -1); + return getInt(name, -1); + } + + public Integer getInt(String name, int fallback) { + Integer value = (Integer)sharedPref.getInt(name, fallback); return (value != -1 ? value : null); } @@ -24,4 +35,30 @@ public class SharedPrefHelper { editor.commit(); } } + + public ArrayList getStringList(String name) { + try { + String json = sharedPref.getString(name, null); + if (json != null) { + JSONArray parsed = new JSONArray(json); + ArrayList restored = new ArrayList<>(parsed.length()); + for (int i = 0; i < parsed.length(); i++) { + restored.add(parsed.getString(i)); + } + return restored; + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return null; + } + + public void setStringList(String name, ArrayList list) { + SharedPreferences.Editor editor = sharedPref.edit().putString(name, new JSONArray(list).toString()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + editor.apply(); + } else { + editor.commit(); + } + } } diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/ApiUtils.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/ApiUtils.java index 8f6a7b5..0b45101 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/ApiUtils.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/ApiUtils.java @@ -14,14 +14,19 @@ public class ApiUtils { } } + public static Boolean isInternalUrl(Uri url) { + return url.toString().startsWith("file:///android_asset/"); + } + public static void openOrShareUrl(Context context, Uri url) { - try { - // Open the URL externally - context.startActivity(new Intent(Intent.ACTION_VIEW, url)); - } catch (ActivityNotFoundException ignored) { - // No app can handle it, so share it instead - context.startActivity(new Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, url.toString())); + if (!isInternalUrl(url)) { + try { // Open the URL externally + context.startActivity(new Intent(Intent.ACTION_VIEW, url)); + return; + } catch (ActivityNotFoundException ignored) {} } + // No app can handle it, so share it instead + context.startActivity(new Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, url.toString())); } public static void writeToClipboard(Context context, String text) { diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/FileUtils.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/FileUtils.java index e89508e..33e1da7 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/FileUtils.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/FileUtils.java @@ -6,6 +6,7 @@ import android.content.Context; import android.net.Uri; import android.os.Build; import android.webkit.CookieManager; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -67,6 +68,24 @@ public class FileUtils { } } + /* https://gist.github.com/defHLT/3ac50c765f3cf289da03 */ + public static String inputStreamToString(InputStream inputStream) { + String res = null; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] b = new byte[1]; + try { + while (inputStream.read(b) != -1) { + outputStream.write(b); + } + res = outputStream.toString(); + inputStream.close(); + outputStream.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return res; + } + // TODO: Handle downloads internally on old Android versions public static void startFileDownload(Context context, Uri downloadUrl, String userAgent, String contentDisposition, String mimeType) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD && !downloadUrl.toString().toLowerCase().startsWith("data:")) { diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/StorageUtils.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/StorageUtils.java index da45209..0363b30 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/StorageUtils.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/utils/StorageUtils.java @@ -6,6 +6,7 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.Environment; import java.io.File; + import org.eu.spacc.spaccdotweb.android.Constants.*; public class StorageUtils { diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/DownloadListener.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/DownloadListener.java index 56d3dde..4d85aaa 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/DownloadListener.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/DownloadListener.java @@ -2,6 +2,7 @@ package org.eu.spacc.spaccdotweb.android.webview; import android.content.Context; import android.net.Uri; + import org.eu.spacc.spaccdotweb.android.utils.FileUtils; public class DownloadListener implements android.webkit.DownloadListener { diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebChromeClient.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebChromeClient.java index 5bdd846..072d7a4 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebChromeClient.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebChromeClient.java @@ -1,24 +1,37 @@ package org.eu.spacc.spaccdotweb.android.webview; import android.annotation.TargetApi; +import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; +import android.webkit.PermissionRequest; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebView; +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eu.spacc.spaccdotweb.android.Config; import org.eu.spacc.spaccdotweb.android.Constants; import org.eu.spacc.spaccdotweb.android.SpaccWebViewActivity; +import org.eu.spacc.spaccdotweb.android.utils.ApiUtils; public class SpaccWebChromeClient extends WebChromeClient { private final SpaccWebViewActivity activity; + private Config config; public SpaccWebChromeClient(SpaccWebViewActivity activity) { super(); this.activity = activity; } + public void applyConfig(Config config) { + this.config = config; + } + // TODO: Android < 4 support @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -37,4 +50,27 @@ public class SpaccWebChromeClient extends WebChromeClient { activity.fileUploadCallback = valueCallback; activity.startActivityForResult(Intent.createChooser(intent, null), Constants.ActivityCodes.UPLOAD_FILE.ordinal()); } + + @Override + public void onPermissionRequest(PermissionRequest request) { + AtomicBoolean handled = new AtomicBoolean(false); + ApiUtils.apiRun(21, () -> { + ArrayList granted = new ArrayList<>(); + for (String resource: request.getResources()) { + if ((resource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID) && config.getAllowDrmMedia()) || + (resource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE) && config.getAllowAudioCapture()) || + (resource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE) && config.getAllowVideoCapture()) + ) { + granted.add(resource); + } + } + if (!granted.isEmpty()) { + request.grant(granted.toArray(new String[0])); + handled.set(true); + } + }); + if (!handled.get()) { + super.onPermissionRequest(request); + } + } } diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebView.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebView.java index fb00899..33c0bbc 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebView.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebView.java @@ -2,10 +2,15 @@ package org.eu.spacc.spaccdotweb.android.webview; import android.content.Context; import android.net.Uri; +import android.os.Build; import android.util.AttributeSet; import android.view.ContextMenu; +import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; +import android.webkit.WebViewClient; +import java.util.ArrayList; + import org.eu.spacc.spaccdotweb.android.Config; import org.eu.spacc.spaccdotweb.android.SpaccWebViewActivity; import org.eu.spacc.spaccdotweb.android.helpers.ConfigReader; @@ -14,36 +19,111 @@ import org.eu.spacc.spaccdotweb.android.utils.ApiUtils; public class SpaccWebView extends WebView { private Config config; private Context context; + private SpaccWebViewClient webViewClient; + private SpaccWebChromeClient webChromeClient; + + private int openString; + private int openExternallyString; + private int copyUrlString; + + private Boolean isLoaded = false; + protected ArrayList scriptQueue = new ArrayList<>(); + + public SpaccWebView(Context context) { + super(context); + setup(context); + } public SpaccWebView(Context context, AttributeSet attrs) { super(context, attrs); + setup(context); + } + + private void setup(Context context) { this.context = context; - this.setWebViewClient(new SpaccWebViewClient(context)); - this.setWebChromeClient(new SpaccWebChromeClient((SpaccWebViewActivity)context)); + this.setWebViewClient(this.webViewClient = new SpaccWebViewClient(context)); + this.setWebChromeClient(this.webChromeClient = new SpaccWebChromeClient((SpaccWebViewActivity)context)); this.setDownloadListener(new DownloadListener(context)); this.config = new Config(); this.applyConfig(context); } + public void setStrings(int open, int openExternally, int copyUrl) { + openString = open; + openExternallyString = openExternally; + copyUrlString = copyUrl; + } + + @Override + public void setWebViewClient(WebViewClient client) { + super.setWebViewClient(client); + webViewClient = (SpaccWebViewClient)client; + webViewClient.applyConfig(config); + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + super.setWebChromeClient(client); + webChromeClient = (SpaccWebChromeClient)client; + webChromeClient.applyConfig(config); + } + // TODO: Implement context menu (long-press on links, images, etc...) @Override protected void onCreateContextMenu(ContextMenu menu) { super.onCreateContextMenu(menu); HitTestResult result = getHitTestResult(); - switch (result.getType()) { + /*switch (result.getType()) { + case HitTestResult.UNKNOWN_TYPE: + case HitTestResult.IMAGE_TYPE: case HitTestResult.SRC_ANCHOR_TYPE: - case HitTestResult.SRC_IMAGE_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; + if (href != null) { + menu.setHeaderTitle(href); + menu.add(openString).setOnMenuItemClickListener(menuItem -> { + if (!webViewClient.shouldOverrideUrlLoading(this, href)) { + this.loadUrl(href); + } + return false; + }); + if (!ApiUtils.isInternalUrl(Uri.parse(href))) { + menu.add(openExternallyString).setOnMenuItemClickListener(menuItem -> { + ApiUtils.openOrShareUrl(context, Uri.parse(href)); + return false; + }); + } + menu.add(copyUrlString).setOnMenuItemClickListener(menuItem -> { + ApiUtils.writeToClipboard(context, href); + return false; + }); + } + /*break; + }*/ + } + + public void injectScript(String script) { + if (isLoaded) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + this.evaluateJavascript(script, null); + } else { + this.loadUrl("javascript:(function(){" + script + "})();"); + } + } else { + scriptQueue.add(script); + } + } + + public void injectStyle(String style) { + injectScript("document.head.appendChild(Object.assign(document.createElement('style'),{innerHTML:\"" + style + "\"}))"); + } + + protected void setLoaded(Boolean loaded) { + if (isLoaded = loaded) { + for (String script : scriptQueue) { + injectScript(script); + } + scriptQueue.clear(); } } @@ -61,11 +141,16 @@ public class SpaccWebView extends WebView { ApiUtils.apiRun(3, () -> webSettings.setAllowFileAccess(false)); + ApiUtils.apiRun(17, () -> webSettings.setMediaPlaybackRequiresUserGesture(!config.getAllowAutoplay())); + 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())); + + webViewClient.applyConfig(config); + webChromeClient.applyConfig(config); } public void loadConfig(Context context, int configResource) { @@ -73,6 +158,10 @@ public class SpaccWebView extends WebView { this.applyConfig(context); } + public Config getConfig() { + return config; + } + public void loadAppIndex() { String url = null; switch (config.getAppIndex()) { diff --git a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebViewClient.java b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebViewClient.java index d6349dd..6d8ae5c 100644 --- a/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebViewClient.java +++ b/SpaccDotWeb.Android/app/src/main/java/org/eu/spacc/spaccdotweb/android/webview/SpaccWebViewClient.java @@ -2,31 +2,49 @@ package org.eu.spacc.spaccdotweb.android.webview; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.net.Uri; import android.webkit.WebView; import android.webkit.WebViewClient; - -import org.eu.spacc.spaccdotweb.android.utils.ApiUtils; - import java.net.URISyntaxException; import java.util.Arrays; -import java.util.List; + +import org.eu.spacc.spaccdotweb.android.Config; +import org.eu.spacc.spaccdotweb.android.Constants; +import org.eu.spacc.spaccdotweb.android.utils.ApiUtils; public class SpaccWebViewClient extends WebViewClient { private final Context context; + private Config config; public SpaccWebViewClient(Context context) { super(); this.context = context; } + public void applyConfig(Config config) { + this.config = config; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + ((SpaccWebView)view).setLoaded(false); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + ((SpaccWebView)view).setLoaded(true); + } + @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // TODO: This should not override all HTTP links if the app loads from remote (which will allow proper internal navigation and file downloads) // NOTE: It seems like the WebView overrides loading of data: URIs before we can get it here... - List externalProtocols = Arrays.asList("data", "http", "https", "mailto", "ftp"); + // List externalProtocols = Arrays.asList("data", "http", "https", "mailto", "ftp"); String protocol = url.toLowerCase().split(":")[0]; - if (protocol.equals("file")) { + if (protocol.equals("file") || (config.getAppIndex() == Constants.AppIndex.REMOTE && Arrays.asList("http", "https").contains(protocol))) { return super.shouldOverrideUrlLoading(view, url); } else if (protocol.equals("intent")) { ApiUtils.apiRun(4, () -> { @@ -42,6 +60,5 @@ public class SpaccWebViewClient extends WebViewClient { ApiUtils.openOrShareUrl(context, Uri.parse(url)); return true; } - } } diff --git a/SpaccDotWeb.Android/app/src/main/res/layout/activity_main.xml b/SpaccDotWeb.Android/app/src/main/res/layout/activity_main.xml index dd37b98..89a83e7 100644 --- a/SpaccDotWeb.Android/app/src/main/res/layout/activity_main.xml +++ b/SpaccDotWeb.Android/app/src/main/res/layout/activity_main.xml @@ -2,6 +2,7 @@ diff --git a/SpaccDotWeb.Android/app/src/main/res/menu/main.xml b/SpaccDotWeb.Android/app/src/main/res/menu/main.xml new file mode 100644 index 0000000..3c2567b --- /dev/null +++ b/SpaccDotWeb.Android/app/src/main/res/menu/main.xml @@ -0,0 +1,13 @@ + +

+ + + + + + + + + + + \ No newline at end of file diff --git a/SpaccDotWeb.Android/app/src/main/res/values-it/strings.xml b/SpaccDotWeb.Android/app/src/main/res/values-it/strings.xml index 1602d42..3cdec3b 100644 --- a/SpaccDotWeb.Android/app/src/main/res/values-it/strings.xml +++ b/SpaccDotWeb.Android/app/src/main/res/values-it/strings.xml @@ -1,6 +1,44 @@ + + + + + + + + + + + +]> - Esci + &about_app; + โ„น๏ธ &about_app; + Yes + No + Cancel + &exit; + ๐Ÿšช &exit; + &hide; + ๐Ÿ•ณ๏ธ &hide; Sposta Dati App La app รจ stata trasferita su una diversa locazione di archiviazione. I dati saranno spostati ora. + Caricamento + &reload; + ๐Ÿ”„ &reload; + &stop_loading; + ๐Ÿ›‘ &stop_loading; + &backwards; + โฎ๏ธ &backwards; + &forward; + โญ๏ธ &forward; + &open; + ๐Ÿ‘Œ &open; + &open_externally; + โ˜๏ธ &open_externally; + ©_url; + ๐Ÿ”— ©_url; + &execute_javascript; + ๐Ÿ”ฃ &execute_javascript; \ No newline at end of file diff --git a/SpaccDotWeb.Android/app/src/main/res/values/strings.xml b/SpaccDotWeb.Android/app/src/main/res/values/strings.xml index e02a8f5..2667c29 100644 --- a/SpaccDotWeb.Android/app/src/main/res/values/strings.xml +++ b/SpaccDotWeb.Android/app/src/main/res/values/strings.xml @@ -1,7 +1,46 @@ + + + + + + + + + + + + +]> - SpaccWebView Application - Exit + &app_name; + &about_app; + โ„น๏ธ &about_app; + Yes + No + Cancel + &exit; + ๐Ÿšช &exit; + &hide; + ๐Ÿ•ณ๏ธ &hide; Move App Data The app has been transferred to a different storage location. The data will be moved now. + Loading + &reload; + ๐Ÿ”„ &reload; + &stop_loading; + ๐Ÿ›‘ &stop_loading; + &backwards; + โฎ๏ธ &backwards; + &forward; + โญ๏ธ &forward; + &open; + ๐Ÿ‘Œ &open; + &open_externally; + โ˜๏ธ &open_externally; + ©_url; + ๐Ÿ”— ©_url; + &execute_javascript; + ๐Ÿ”ฃ &execute_javascript; \ No newline at end of file diff --git a/SpaccDotWeb.Android/app/src/main/res/xml/app_config.xml b/SpaccDotWeb.Android/app/src/main/res/xml/app_config.xml index 036e435..235b50a 100644 --- a/SpaccDotWeb.Android/app/src/main/res/xml/app_config.xml +++ b/SpaccDotWeb.Android/app/src/main/res/xml/app_config.xml @@ -1,8 +1,21 @@ + + local index.html - https://example.com + https://example.com/ + + true + true + true + true + false + false + + false + false + \ No newline at end of file diff --git a/SpaccDotWeb.Android/settings.gradle b/SpaccDotWeb.Android/settings.gradle index cc1c385..07c9759 100644 --- a/SpaccDotWeb.Android/settings.gradle +++ b/SpaccDotWeb.Android/settings.gradle @@ -9,6 +9,7 @@ pluginManagement { plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -16,5 +17,6 @@ dependencyResolutionManagement { mavenCentral() } } + rootProject.name = 'SpaccWebView Application' include ':app'