[SpaccWebView Android] Initial external storage support, JS alerts working, other stuff
This commit is contained in:
parent
fd0151b999
commit
b582399d06
|
@ -20,6 +20,7 @@ android {
|
|||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,23 @@
|
|||
<!-- Lets the app access the Internet — not needed for fully offline apps -->
|
||||
<!-- <uses-permission android:name="android.permission.INTERNET" /> -->
|
||||
|
||||
<!-- Lets the webview load and display files from the device's shared storage -->
|
||||
<!-- <uses-permission android:name="android.permission.READ_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 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- Additional suggested attributes:
|
||||
android:appCategory=["accessibility" | "audio" | "game" | "image" | "maps" | "news" | "productivity" | "social" | "video"]
|
||||
android:isGame=["true" | "false"]
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:usesCleartextTraffic=["true" | "false"]
|
||||
-->
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
|
||||
android:resizeableActivity="true"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
|
|
|
@ -1,3 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<h1>SpaccWebView Application</h1>
|
||||
<p><a href="https://gitlab.com/SpaccInc/SpaccDotWeb">https://gitlab.com/SpaccInc/SpaccDotWeb</a></p>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<h1>SpaccWebView Example Android Application</h1>
|
||||
<p>Repository: <a href="https://gitlab.com/SpaccInc/SpaccDotWeb">https://gitlab.com/SpaccInc/SpaccDotWeb</a>.</p>
|
||||
<h2>Tests</h2>
|
||||
<h3>JavaScript</h3>
|
||||
<p>
|
||||
<label><input type="text" autocomplete="off" onkeypress="(function(event){if(event.keyCode===13)eval(event.target.value);})(event);"/></label>
|
||||
<button onclick="eval(this.parentElement.querySelector('input').value);">Run</button>
|
||||
</p>
|
||||
<h3>Popups</h3>
|
||||
<p>
|
||||
<button onclick="alert(1);">alert()</button>
|
||||
<button onclick="confirm(1);">confirm()</button>
|
||||
<button onclick="prompt(1);">prompt()</button>
|
||||
</p>
|
||||
<h3>Links</h3>
|
||||
<ul>
|
||||
<li><a href="file:///">file://</a></li>
|
||||
<li><a href="http://example.com">http://</a></li>
|
||||
<li><a href="https://example.com">https://</a></li>
|
||||
<li><a href="ftp://example.com">ftp://</a></li>
|
||||
<li><a href="mailto:example@example.com">mailto:</a></li>
|
||||
</ul>
|
||||
<h3>Files</h3>
|
||||
<p>Upload: <label><input type="file"/></label></p>
|
||||
<p>Download: <a download="test.bin" href="data:text/plain;utf8,Test">Download</a></p>
|
||||
</body>
|
||||
</html>
|
|
@ -1,28 +1,18 @@
|
|||
package com.example.spaccwebviewapplication;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import org.eu.spacc.spaccdotweb.android.*;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
private SpaccWebView webView;
|
||||
public class MainActivity extends SpaccWebViewActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
webView = findViewById(R.id.webview);
|
||||
|
||||
webView.loadAppIndex();
|
||||
}
|
||||
DataMoveHelper.run(this, R.string.exit, R.string.move_app_data, R.string.move_app_data_info);
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
this.webView = findViewById(R.id.webview);
|
||||
this.webView.loadAppIndex();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package com.example.spaccwebviewapplication;
|
||||
|
||||
import org.eu.spacc.spaccdotweb.android.SpaccWebViewApplication;
|
||||
|
||||
public class MainApplication extends SpaccWebViewApplication {
|
||||
// This proxy class can be left empty,
|
||||
// it exists just to provide an unique android:name for the manifest
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
public class Constants {
|
||||
public static enum AppIndex { LOCAL, REMOTE }
|
||||
public enum AppIndex { LOCAL, REMOTE }
|
||||
public enum DataLocation { INTERNAL, EXTERNAL }
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
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;
|
||||
|
||||
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");
|
||||
if (dataLocationSaved == null) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static void restartActivity(Context context) {
|
||||
Activity activity = (Activity)context;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
activity.recreate();
|
||||
} else {
|
||||
Intent intent = activity.getIntent();
|
||||
activity.finish();
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
public static void moveDirectory(File sourceLocation, File targetLocation, boolean deleteRoot) throws IOException {
|
||||
copyDirectory(sourceLocation, targetLocation);
|
||||
recursiveDelete(sourceLocation, deleteRoot);
|
||||
}
|
||||
|
||||
/* https://subversivebytes.wordpress.com/2012/11/05/java-copy-directory-recursive-delete/ */
|
||||
|
||||
public static void copyDirectory(File sourceLocation, File targetLocation) throws IOException {
|
||||
if (sourceLocation.isDirectory()) {
|
||||
if (!targetLocation.exists()) {
|
||||
targetLocation.mkdir();
|
||||
}
|
||||
String[] children = sourceLocation.list();
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
copyDirectory(new File(sourceLocation, children[i]), new File(targetLocation, children[i]));
|
||||
}
|
||||
}
|
||||
else {
|
||||
InputStream in = new FileInputStream(sourceLocation);
|
||||
OutputStream out = new FileOutputStream(targetLocation);
|
||||
try {
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while((len = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
} finally {
|
||||
in.close();
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void recursiveDelete(File rootDir) {
|
||||
recursiveDelete(rootDir, true);
|
||||
}
|
||||
|
||||
public static void recursiveDelete(File rootDir, boolean deleteRoot) {
|
||||
File[] childDirs = rootDir.listFiles();
|
||||
for (File childDir : childDirs) {
|
||||
if (childDir.isFile()) {
|
||||
childDir.delete();
|
||||
} else {
|
||||
recursiveDelete(childDir, deleteRoot);
|
||||
childDir.delete();
|
||||
}
|
||||
}
|
||||
if (deleteRoot) {
|
||||
rootDir.delete();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
||||
public class SharedPrefHelper {
|
||||
private final SharedPreferences sharedPref;
|
||||
|
||||
public SharedPrefHelper(Context context) {
|
||||
this.sharedPref = context.getSharedPreferences("SpaccWebView", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public Integer getInt(String name) {
|
||||
Integer value = (Integer)sharedPref.getInt(name, -1);
|
||||
return (value != -1 ? value : null);
|
||||
}
|
||||
|
||||
public void setInt(String name, int value) {
|
||||
SharedPreferences.Editor editor = sharedPref.edit().putInt(name, value);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
|
||||
editor.apply();
|
||||
} else {
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebView;
|
||||
|
||||
public class SpaccWebChromeClient extends WebChromeClient {
|
||||
private static final int INPUT_FILE_REQUEST_CODE = 1;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SpaccWebChromeClient(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;
|
||||
}
|
||||
}
|
|
@ -11,23 +11,24 @@ public class SpaccWebView extends WebView {
|
|||
@SuppressLint("SetJavaScriptEnabled")
|
||||
public SpaccWebView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.setWebViewClient(new SpaccWebViewClient(context));
|
||||
this.setWebChromeClient(new SpaccWebChromeClient(context));
|
||||
|
||||
WebSettings webSettings = getSettings();
|
||||
WebSettings webSettings = this.getSettings();
|
||||
|
||||
webSettings.setJavaScriptEnabled(Config.ALLOW_JAVASCRIPT);
|
||||
|
||||
ApiUtils.apiRun(7, () -> webSettings.setDomStorageEnabled(Config.ALLOW_STORAGE));
|
||||
ApiUtils.apiRun(5, () -> webSettings.setDatabaseEnabled(Config.ALLOW_STORAGE));
|
||||
if (Config.ALLOW_STORAGE) {
|
||||
ApiUtils.apiRun(5, () -> webSettings.setDatabasePath(context.getFilesDir().getParent() + "/databases"));
|
||||
ApiUtils.apiRun(5, () -> webSettings.setDatabasePath(context.getDir("databases", 0).getAbsolutePath()));
|
||||
}
|
||||
|
||||
ApiUtils.apiRun(3, () -> webSettings.setAllowFileAccess(true));
|
||||
ApiUtils.apiRun(3, () -> webSettings.setAllowFileAccess(false));
|
||||
}
|
||||
|
||||
public void loadAppIndex() {
|
||||
String url = null;
|
||||
|
||||
switch (Config.APP_INDEX) {
|
||||
case LOCAL:
|
||||
url = ("file:///android_asset/" + Config.LOCAL_INDEX);
|
||||
|
@ -36,7 +37,6 @@ public class SpaccWebView extends WebView {
|
|||
url = Config.REMOTE_INDEX;
|
||||
break;
|
||||
}
|
||||
|
||||
loadUrl(url);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import java.io.File;
|
||||
|
||||
public class SpaccWebViewActivity extends Activity {
|
||||
protected SpaccWebView webView;
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (this.webView.canGoBack()) {
|
||||
this.webView.goBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi") // We have our custom implementation
|
||||
@Override
|
||||
public File getDataDir() {
|
||||
return getApplicationContext().getDataDir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDir(String name, int mode) {
|
||||
return getApplicationContext().getDir(name, mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFilesDir() {
|
||||
return getApplicationContext().getFilesDir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getCacheDir() {
|
||||
return getApplicationContext().getCacheDir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDatabasePath(String name) {
|
||||
return getApplicationContext().getDatabasePath(name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
import android.app.Application;
|
||||
import java.io.File;
|
||||
|
||||
public class SpaccWebViewApplication extends Application {
|
||||
|
||||
@Override
|
||||
public File getDataDir() {
|
||||
return StorageUtils.getDataDir(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDir(String name, int mode) {
|
||||
File dir = new File(getDataDir(), name);
|
||||
dir.mkdirs();
|
||||
return dir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFilesDir() {
|
||||
return getDir("files", 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getCacheDir() {
|
||||
return getDir("cache", 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDatabasePath(String name) {
|
||||
// TODO: should this be "app_databases"?
|
||||
return new File(getDir("databases", 0), name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package org.eu.spacc.spaccdotweb.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
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 {
|
||||
|
||||
public static boolean isInstalledOnExternalStorage(Context context) {
|
||||
try {
|
||||
int flags = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).applicationInfo.flags;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/ApplicationInfo.java#2516
|
||||
return ((flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static File getProtectedDataDir(Context context) {
|
||||
// Usually is /data/data/<package>
|
||||
return new File(Environment.getDataDirectory() + File.separator + "data" + File.separator + context.getPackageName());
|
||||
}
|
||||
|
||||
public static File getInternalDataDir(Context context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
|
||||
// Usually is /sdcard/Android/data/<package>
|
||||
return getParentDir(context.getExternalFilesDir(null));
|
||||
} else {
|
||||
// TODO: This can actually be external storage on old Androids, we should make it return null in those cases
|
||||
return new File(Environment.getExternalStorageDirectory() + "Android" + File.separator + "data" + File.separator + context.getPackageName());
|
||||
}
|
||||
}
|
||||
|
||||
public static File getExternalDataDir(Context context) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs.length >= 2) {
|
||||
return getParentDir(dirs[1]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// TODO: We need hacks for old Android systems which have emulated internal + real external storages
|
||||
return getInternalDataDir(context);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This should not suggest to use external storage if we don't have the necessary manifest permission
|
||||
public static File getDataDir(Context context) {
|
||||
File dir = null;
|
||||
if (isInstalledOnExternalStorage(context)) {
|
||||
dir = getExternalDataDir(context);
|
||||
}
|
||||
if (dir == null) {
|
||||
dir = getProtectedDataDir(context);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
public static File dataDirFromEnum(Context context, DataLocation dataLocation) {
|
||||
switch (dataLocation) {
|
||||
case INTERNAL:
|
||||
return getProtectedDataDir(context);
|
||||
case EXTERNAL:
|
||||
return getExternalDataDir(context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static File getParentDir(File path) {
|
||||
return (path != null ? path.getParentFile() : null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="exit">Esci</string>
|
||||
<string name="move_app_data">Sposta Dati App</string>
|
||||
<string name="move_app_data_info">La app è stata trasferita su una diversa locazione di archiviazione. I dati saranno spostati ora.</string>
|
||||
</resources>
|
|
@ -1,3 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">SpaccWebView Application</string>
|
||||
<string name="app_name" translatable="false">SpaccWebView Application</string>
|
||||
<string name="exit">Exit</string>
|
||||
<string name="move_app_data">Move App Data</string>
|
||||
<string name="move_app_data_info">The app has been transferred to a different storage location. The data will be moved now.</string>
|
||||
</resources>
|
Loading…
Reference in New Issue