[SpaccWebView.Android] Add file downloads, initial work for other things

This commit is contained in:
octospacc 2024-11-09 17:50:48 +01:00
parent b582399d06
commit 04b053cbc2
19 changed files with 221 additions and 64 deletions

View File

@ -8,7 +8,7 @@
<!-- <uses-permission android:name="android.permission.INTERNET" /> --> <!-- <uses-permission android:name="android.permission.INTERNET" /> -->
<!-- Needed from Android ??? to 4.4 KitKat (API ???-19) to keep app data on external storage --> <!-- Needed from Android ??? to 4.4 KitKat (API ???-19) to keep app data on external storage -->
<!-- Removing these will not break the app, but it will write only on internal storage --> <!-- Removing these will not break the app, but it will write only on internal storage on those versions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

View File

@ -21,10 +21,18 @@
<li><a href="http://example.com">http://</a></li> <li><a href="http://example.com">http://</a></li>
<li><a href="https://example.com">https://</a></li> <li><a href="https://example.com">https://</a></li>
<li><a href="ftp://example.com">ftp://</a></li> <li><a href="ftp://example.com">ftp://</a></li>
<li><a href="data:text/plain;utf8,Hello World!">data:text/plain</a></li>
<li><a href="data:text/html;utf8,&lt;h2&gt;Hello World!">data:text/html</a></li>
<li><a href="mailto:example@example.com">mailto:</a></li> <li><a href="mailto:example@example.com">mailto:</a></li>
</ul> </ul>
<h3>Files</h3> <h3>Files</h3>
<p>Upload: <label><input type="file"/></label></p> <p>Upload: <label><input type="file"/></label></p>
<p>Download: <a download="test.bin" href="data:text/plain;utf8,Test">Download</a></p> <p>Download:</p>
<ul>
<li><a download="Hello World.txt" href="data:text/plain;utf8,Hello World!">data:, .txt</a></li>
<li><a download="Example.html" href="http://example.com/index.html">http://, .html</a></li>
<li><a download="Example.html" href="https://example.com/index.html">https://, .html</a></li>
<li><a href="https://hlb0.octt.eu.org/Drive/Misc/onestop.mid">https://, .mid</a></li>
</ul>
</body> </body>
</html> </html>

View File

@ -1,7 +1,9 @@
package com.example.spaccwebviewapplication; package com.example.spaccwebviewapplication;
import android.os.Bundle; import android.os.Bundle;
import org.eu.spacc.spaccdotweb.android.*;
import org.eu.spacc.spaccdotweb.android.helpers.DataMoveHelper;
import org.eu.spacc.spaccdotweb.android.SpaccWebViewActivity;
public class MainActivity extends SpaccWebViewActivity { public class MainActivity extends SpaccWebViewActivity {

View File

@ -1,12 +0,0 @@
package org.eu.spacc.spaccdotweb.android;
import android.os.Build;
public class ApiUtils {
public static void apiRun(int apiLevel, Runnable action) {
if (Build.VERSION.SDK_INT >= apiLevel) {
action.run();
}
}
}

View File

@ -1,6 +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 AppIndex { LOCAL, REMOTE } public enum AppIndex { LOCAL, REMOTE }
public enum DataLocation { INTERNAL, EXTERNAL } public enum DataLocation { INTERNAL, EXTERNAL }
} }

View File

@ -2,11 +2,23 @@ package org.eu.spacc.spaccdotweb.android;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
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;
// @Override
// 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) {
// Uri fileUri = data.getData();
// if (fileUri != null) {
// enqueueDownload(Uri.parse(fileUri.toString()));
// }
// }
// }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (this.webView.canGoBack()) { if (this.webView.canGoBack()) {
@ -16,6 +28,16 @@ public class SpaccWebViewActivity extends Activity {
} }
} }
// // TODO: Find some way to download to any storage location with DownloadManager, since it doesn't take content:// URIs
// private void enqueueDownload(Uri fileUri) {
// DownloadDataHolder downloadDataHolder = DownloadDataHolder.getInstance();
// FileUtils.startFileDownload(this,
// downloadDataHolder.getDownloadUrl(),
// downloadDataHolder.getContentDisposition(),
// downloadDataHolder.getUserAgent(),
// downloadDataHolder.getMimeType());
// }
@SuppressLint("NewApi") // We have our custom implementation @SuppressLint("NewApi") // We have our custom implementation
@Override @Override
public File getDataDir() { public File getDataDir() {

View File

@ -1,6 +1,7 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android;
import android.app.Application; import android.app.Application;
import org.eu.spacc.spaccdotweb.android.utils.StorageUtils;
import java.io.File; import java.io.File;
public class SpaccWebViewApplication extends Application { public class SpaccWebViewApplication extends Application {

View File

@ -1,37 +0,0 @@
package org.eu.spacc.spaccdotweb.android;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.util.Arrays;
import java.util.List;
public class SpaccWebViewClient extends WebViewClient {
private final Context context;
public SpaccWebViewClient(Context context) {
super();
this.context = context;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// TODO: This should not override all HTTP links if the app loads from remote
List<String> protocols = Arrays.asList("http", "https", "mailto", "ftp");
if (protocols.contains(url.toLowerCase().split(":")[0])) {
try {
// Open the link externally
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(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));
}
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
}

View File

@ -1,4 +1,4 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android.helpers;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
@ -6,6 +6,11 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
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; import java.io.IOException;
public class DataMoveHelper { public class DataMoveHelper {

View File

@ -1,4 +1,4 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android.helpers;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;

View File

@ -0,0 +1,26 @@
package org.eu.spacc.spaccdotweb.android.utils;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
public class ApiUtils {
public static void apiRun(int apiLevel, Runnable action) {
if (Build.VERSION.SDK_INT >= apiLevel) {
action.run();
}
}
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()));
}
}
}

View File

@ -1,5 +1,12 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android.utils;
import static android.content.Context.DOWNLOAD_SERVICE;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.webkit.CookieManager;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -60,4 +67,21 @@ public class FileUtils {
rootDir.delete(); rootDir.delete();
} }
} }
// 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:")) {
// TODO: We should handle downloading data: URIs manually
DownloadManager.Request request = new DownloadManager.Request(downloadUrl)
//.setDestinationUri(fileUri)
.setMimeType(mimeType)
.addRequestHeader("User-Agent", userAgent)
.addRequestHeader("Content-Disposition", contentDisposition)
.addRequestHeader("Cookie", CookieManager.getInstance().getCookie(downloadUrl.toString()));
ApiUtils.apiRun(11, () -> request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED));
((DownloadManager)context.getSystemService(DOWNLOAD_SERVICE)).enqueue(request);
} else {
ApiUtils.openOrShareUrl(context, downloadUrl);
}
}
} }

View File

@ -1,4 +1,4 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android.utils;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;

View File

@ -0,0 +1,46 @@
package org.eu.spacc.spaccdotweb.android.webview;
import android.net.Uri;
public class DownloadDataHolder {
private static DownloadDataHolder instance;
private Uri downloadUrl;
private String userAgent;
private String contentDisposition;
private String mimeType;
public static synchronized DownloadDataHolder getInstance() {
if (instance == null) {
instance = new DownloadDataHolder();
}
return instance;
}
public void setData(Uri downloadUrl, String userAgent, String contentDisposition, String mimeType, long contentLength) {
this.downloadUrl = downloadUrl;
this.userAgent = userAgent;
this.contentDisposition = contentDisposition;
this.mimeType = mimeType;
}
public void clearData() {
instance = new DownloadDataHolder();
}
public Uri getDownloadUrl() {
return downloadUrl;
}
public String getUserAgent() {
return userAgent;
}
public String getContentDisposition() {
return contentDisposition;
}
public String getMimeType() {
return mimeType;
}
}

View File

@ -0,0 +1,27 @@
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 {
private final Context context;
public DownloadListener(Context context) {
this.context = context;
}
// TODO: Read file name from download="..." HTML <a> attribute when present
// TODO: Implement file destination path picking (requires Android < 5 with SAF Intent)
@Override
public void onDownloadStart(String downloadUrl, String userAgent, String contentDisposition, String mimeType, long contentLength) {
// String[] nameParts = downloadUrl.split("/");
// Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
// .addCategory(Intent.CATEGORY_OPENABLE)
// .setType(mimeType)
// .putExtra(Intent.EXTRA_TITLE, nameParts[nameParts.length - 1]);
// DownloadDataHolder.getInstance().setData(Uri.parse(downloadUrl), userAgent, contentDisposition, mimeType, contentLength);
// ((Activity)context).startActivityForResult(intent, Constants.CREATE_FILE_REQUEST_CODE);
FileUtils.startFileDownload(context, Uri.parse(downloadUrl), userAgent, contentDisposition, mimeType);
}
}

View File

@ -1,18 +1,23 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android.webview;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
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.utils.ApiUtils;
import org.eu.spacc.spaccdotweb.android.Config;
public class SpaccWebView extends WebView { public class SpaccWebView extends WebView {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
public SpaccWebView(Context context, AttributeSet attrs) { public SpaccWebView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
this.setWebViewClient(new SpaccWebViewClient(context)); this.setWebViewClient(new WebViewClient(context));
this.setWebChromeClient(new SpaccWebChromeClient(context)); this.setWebChromeClient(new WebChromeClient(context));
this.setDownloadListener(new DownloadListener(context));
WebSettings webSettings = this.getSettings(); WebSettings webSettings = this.getSettings();
@ -27,6 +32,12 @@ public class SpaccWebView extends WebView {
ApiUtils.apiRun(3, () -> webSettings.setAllowFileAccess(false)); ApiUtils.apiRun(3, () -> webSettings.setAllowFileAccess(false));
} }
// TODO: Implement context menu (long-press on links, images, etc...)
// @Override
// protected void onCreateContextMenu(ContextMenu menu) {
// super.onCreateContextMenu(menu);
// }
public void loadAppIndex() { public void loadAppIndex() {
String url = null; String url = null;
switch (Config.APP_INDEX) { switch (Config.APP_INDEX) {

View File

@ -1,19 +1,18 @@
package org.eu.spacc.spaccdotweb.android; package org.eu.spacc.spaccdotweb.android.webview;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.webkit.ValueCallback; import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView; import android.webkit.WebView;
public class SpaccWebChromeClient extends WebChromeClient { public class WebChromeClient extends android.webkit.WebChromeClient {
private static final int INPUT_FILE_REQUEST_CODE = 1; private static final int INPUT_FILE_REQUEST_CODE = 1;
private final Context context; private final Context context;
public SpaccWebChromeClient(Context context) { public WebChromeClient(Context context) {
super(); super();
this.context = context; this.context = context;
} }

View File

@ -0,0 +1,34 @@
package org.eu.spacc.spaccdotweb.android.webview;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.webkit.WebView;
import org.eu.spacc.spaccdotweb.android.utils.ApiUtils;
import java.util.Arrays;
import java.util.List;
public class WebViewClient extends android.webkit.WebViewClient {
private final Context context;
public WebViewClient(Context context) {
super();
this.context = context;
}
@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<String> protocols = Arrays.asList("data", "http", "https", "mailto", "ftp");
if (protocols.contains(url.toLowerCase().split(":")[0])) {
ApiUtils.openOrShareUrl(context, Uri.parse(url));
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
}

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".MainActivity">
<org.eu.spacc.spaccdotweb.android.SpaccWebView <org.eu.spacc.spaccdotweb.android.webview.SpaccWebView
android:id="@+id/webview" android:id="@+id/webview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"