mirror of
https://gitlab.com/SpaccInc/SpaccDotWeb.git
synced 2025-06-05 21:29:12 +02:00
[SpaccWebView.Android] Make actual Android library, upgrade to SDK 35
This commit is contained in:
1
SpaccDotWeb.Android/SpaccWebView/.gitignore
vendored
Normal file
1
SpaccDotWeb.Android/SpaccWebView/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
29
SpaccDotWeb.Android/SpaccWebView/build.gradle
Normal file
29
SpaccDotWeb.Android/SpaccWebView/build.gradle
Normal file
@@ -0,0 +1,29 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'org.eu.spacc.spaccwebview.android'
|
||||
|
||||
defaultConfig {
|
||||
minSdk 1
|
||||
compileSdk 35
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// testImplementation 'junit:junit:4.13.2'
|
||||
// androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
// androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package org.eu.spacc.spaccwebview.android;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
assertEquals("org.eu.spacc.spaccwebview.android.test", appContext.getPackageName());
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@@ -0,0 +1,94 @@
|
||||
package org.eu.spacc.spaccwebview.android;
|
||||
|
||||
import org.eu.spacc.spaccwebview.android.Constants.*;
|
||||
import org.eu.spacc.spaccwebview.android.helpers.ConfigReader;
|
||||
|
||||
public class Config extends Defaults {
|
||||
private ConfigReader configReader;
|
||||
|
||||
public Config() {}
|
||||
|
||||
public Config(ConfigReader configReader) {
|
||||
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);
|
||||
}
|
||||
|
||||
public Boolean getAllowStorage() {
|
||||
Boolean value = getBoolean("allow_storage");
|
||||
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);
|
||||
}
|
||||
|
||||
public Boolean getDisplayZoomControls() {
|
||||
Boolean value = getBoolean("display_zoom_controls");
|
||||
return (value != null ? value : Defaults.DISPLAY_ZOOM_CONTROLS);
|
||||
}
|
||||
|
||||
public AppIndex getAppIndex() {
|
||||
AppIndex value = (AppIndex)get("app_index");
|
||||
return (value != null ? value : Defaults.APP_INDEX);
|
||||
}
|
||||
|
||||
public String getLocalIndex() {
|
||||
String value = getString("local_index");
|
||||
return (value != null ? value : Defaults.LOCAL_INDEX);
|
||||
}
|
||||
|
||||
public String getRemoteIndex() {
|
||||
return getString("remote_index");
|
||||
}
|
||||
|
||||
public String getStandardFontFamily() {
|
||||
return getString("standard_font_family");
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return getString("user_agent");
|
||||
}
|
||||
|
||||
private Object get(String key) {
|
||||
return (configReader != null
|
||||
? configReader.get(key)
|
||||
: null);
|
||||
}
|
||||
|
||||
private Boolean getBoolean(String key) {
|
||||
return (Boolean)get(key);
|
||||
}
|
||||
|
||||
private String getString(String key) {
|
||||
return (String)get(key);
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package org.eu.spacc.spaccwebview.android;
|
||||
|
||||
public class Constants {
|
||||
public enum ActivityCodes { UPLOAD_FILE /* , DOWNLOAD_FILE */ }
|
||||
public enum AppIndex { LOCAL, REMOTE }
|
||||
public enum DataLocation { INTERNAL, EXTERNAL }
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package org.eu.spacc.spaccwebview.android;
|
||||
|
||||
import org.eu.spacc.spaccwebview.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;
|
||||
public static final String LOCAL_INDEX = "index.html";
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
package org.eu.spacc.spaccwebview.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.webkit.ValueCallback;
|
||||
import java.io.File;
|
||||
|
||||
import org.eu.spacc.spaccwebview.android.webview.SpaccWebChromeClient;
|
||||
import org.eu.spacc.spaccwebview.android.webview.SpaccWebView;
|
||||
|
||||
public class SpaccWebViewActivity extends Activity {
|
||||
protected SpaccWebView webView;
|
||||
public ValueCallback<Uri> fileUploadCallback;
|
||||
public ValueCallback<Uri[]> filesUploadCallback;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.ECLAIR_MR1)
|
||||
@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()));
|
||||
// }
|
||||
// }
|
||||
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
|
||||
public void onBackPressed() {
|
||||
if (this.webView.canGoBack()) {
|
||||
this.webView.goBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
// // 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
|
||||
@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,36 @@
|
||||
package org.eu.spacc.spaccwebview.android;
|
||||
|
||||
import android.app.Application;
|
||||
import org.eu.spacc.spaccwebview.android.utils.StorageUtils;
|
||||
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,60 @@
|
||||
package org.eu.spacc.spaccwebview.android.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import org.eu.spacc.spaccwebview.android.Constants.*;
|
||||
|
||||
public class ConfigReader {
|
||||
private final Map<String, Object> configData = new HashMap<>();
|
||||
|
||||
public ConfigReader(Context context, int configResource) {
|
||||
try {
|
||||
parseConfigData(context, configResource);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Object get(String key) {
|
||||
return configData.get(key);
|
||||
}
|
||||
|
||||
private void parseConfigData(Context context, int configResource) throws IOException, XmlPullParserException {
|
||||
XmlResourceParser parser = context.getResources().getXml(configResource);
|
||||
int eventType = parser.getEventType();
|
||||
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||
if (eventType == XmlResourceParser.START_TAG) {
|
||||
String type = parser.getName();
|
||||
if (!type.equals("config")) {
|
||||
String name = parser.getAttributeValue(null, "name");
|
||||
String value = parser.nextText();
|
||||
configData.put(name, parseValue(type, value));
|
||||
}
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
}
|
||||
|
||||
private Object parseValue(String type, String value) {
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
return Boolean.parseBoolean(value);
|
||||
case "string":
|
||||
return value;
|
||||
default:
|
||||
value = value.toUpperCase();
|
||||
try {
|
||||
switch (type) {
|
||||
case "AppIndex":
|
||||
return AppIndex.valueOf(value);
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package org.eu.spacc.spaccwebview.android.helpers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eu.spacc.spaccwebview.android.Constants;
|
||||
import org.eu.spacc.spaccwebview.android.R;
|
||||
import org.eu.spacc.spaccwebview.android.utils.StorageUtils;
|
||||
import org.eu.spacc.spaccwebview.android.utils.FileUtils;
|
||||
|
||||
public class DataMoveHelper {
|
||||
|
||||
public static void run(Context 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(R.string.move_app_data)
|
||||
.setMessage(R.string.move_app_data_info)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(R.string.exit, (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();
|
||||
}
|
||||
}
|
||||
|
||||
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,64 @@
|
||||
package org.eu.spacc.spaccwebview.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;
|
||||
|
||||
public SharedPrefHelper(Context context) {
|
||||
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) {
|
||||
return getInt(name, -1);
|
||||
}
|
||||
|
||||
public Integer getInt(String name, int fallback) {
|
||||
Integer value = (Integer)sharedPref.getInt(name, fallback);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<String> getStringList(String name) {
|
||||
try {
|
||||
String json = sharedPref.getString(name, null);
|
||||
if (json != null) {
|
||||
JSONArray parsed = new JSONArray(json);
|
||||
ArrayList<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package org.eu.spacc.spaccwebview.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 Boolean isInternalUrl(Uri url) {
|
||||
return url.toString().startsWith("file:///android_asset/");
|
||||
}
|
||||
|
||||
public static void openOrShareUrl(Context context, Uri url) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package org.eu.spacc.spaccwebview.android.utils;
|
||||
|
||||
import static android.content.Context.DOWNLOAD_SERVICE;
|
||||
import android.app.DownloadManager;
|
||||
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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/* 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:")) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
package org.eu.spacc.spaccwebview.android.utils;
|
||||
|
||||
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.spaccwebview.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,46 @@
|
||||
package org.eu.spacc.spaccwebview.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;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
package org.eu.spacc.spaccwebview.android.webview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.eu.spacc.spaccwebview.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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
package org.eu.spacc.spaccwebview.android.webview;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
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.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.eu.spacc.spaccwebview.android.Config;
|
||||
import org.eu.spacc.spaccwebview.android.Constants;
|
||||
import org.eu.spacc.spaccwebview.android.SpaccWebViewActivity;
|
||||
import org.eu.spacc.spaccwebview.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)
|
||||
@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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionRequest(PermissionRequest request) {
|
||||
AtomicBoolean handled = new AtomicBoolean(false);
|
||||
ApiUtils.apiRun(21, () -> {
|
||||
ArrayList<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,169 @@
|
||||
package org.eu.spacc.spaccwebview.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.spaccwebview.android.Config;
|
||||
import org.eu.spacc.spaccwebview.android.R;
|
||||
import org.eu.spacc.spaccwebview.android.SpaccWebViewActivity;
|
||||
import org.eu.spacc.spaccwebview.android.helpers.ConfigReader;
|
||||
import org.eu.spacc.spaccwebview.android.utils.ApiUtils;
|
||||
|
||||
public class SpaccWebView extends WebView {
|
||||
private Config config;
|
||||
private Context context;
|
||||
private SpaccWebViewClient webViewClient;
|
||||
private SpaccWebChromeClient webChromeClient;
|
||||
|
||||
|
||||
private Boolean isLoaded = false;
|
||||
protected ArrayList<String> 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(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);
|
||||
}
|
||||
|
||||
@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()) {
|
||||
case HitTestResult.UNKNOWN_TYPE:
|
||||
case HitTestResult.IMAGE_TYPE:
|
||||
case HitTestResult.SRC_ANCHOR_TYPE:
|
||||
case HitTestResult.SRC_IMAGE_ANCHOR_TYPE:*/
|
||||
String href = result.getExtra();
|
||||
if (href != null) {
|
||||
menu.setHeaderTitle(href);
|
||||
menu.add(R.string.open_menu).setOnMenuItemClickListener(menuItem -> {
|
||||
if (!webViewClient.shouldOverrideUrlLoading(this, href)) {
|
||||
this.loadUrl(href);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!ApiUtils.isInternalUrl(Uri.parse(href))) {
|
||||
menu.add(R.string.open_externally_menu).setOnMenuItemClickListener(menuItem -> {
|
||||
ApiUtils.openOrShareUrl(context, Uri.parse(href));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
menu.add(R.string.copy_url_menu).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();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyConfig(Context context) {
|
||||
WebSettings webSettings = this.getSettings();
|
||||
|
||||
webSettings.setJavaScriptEnabled(config.getAllowJavascript());
|
||||
|
||||
boolean allowStorage = config.getAllowStorage();
|
||||
ApiUtils.apiRun(7, () -> webSettings.setDomStorageEnabled(allowStorage));
|
||||
ApiUtils.apiRun(5, () -> webSettings.setDatabaseEnabled(allowStorage));
|
||||
if (allowStorage) {
|
||||
ApiUtils.apiRun(5, () -> webSettings.setDatabasePath(context.getDir("databases", 0).getAbsolutePath()));
|
||||
}
|
||||
|
||||
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) {
|
||||
this.config = new Config(new ConfigReader(context, configResource));
|
||||
this.applyConfig(context);
|
||||
}
|
||||
|
||||
public Config getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void loadAppIndex() {
|
||||
String url = null;
|
||||
switch (config.getAppIndex()) {
|
||||
case LOCAL:
|
||||
url = ("file:///android_asset/" + config.getLocalIndex());
|
||||
break;
|
||||
case REMOTE:
|
||||
url = config.getRemoteIndex();
|
||||
break;
|
||||
}
|
||||
this.loadUrl(url);
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
package org.eu.spacc.spaccwebview.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 java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.eu.spacc.spaccwebview.android.Config;
|
||||
import org.eu.spacc.spaccwebview.android.Constants;
|
||||
import org.eu.spacc.spaccwebview.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<String> externalProtocols = Arrays.asList("data", "http", "https", "mailto", "ftp");
|
||||
String protocol = url.toLowerCase().split(":")[0];
|
||||
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, () -> {
|
||||
try {
|
||||
// TODO: Should this handle broadcasts and services differently?
|
||||
context.startActivity(Intent.parseUri(url, 0));
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
ApiUtils.openOrShareUrl(context, Uri.parse(url));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE resources [
|
||||
<!ENTITY about_app "Informazioni App">
|
||||
<!ENTITY exit "Esci">
|
||||
<!ENTITY hide "Nascondi">
|
||||
<!ENTITY reload "Ricarica">
|
||||
<!ENTITY stop_loading "Ferma Caricamento">
|
||||
<!ENTITY backwards "Indietro">
|
||||
<!ENTITY forward "Avanti">
|
||||
<!ENTITY open "Apri">
|
||||
<!ENTITY open_externally "&open; Esternamente">
|
||||
<!ENTITY copy_url "Copia URL">
|
||||
<!ENTITY execute_javascript "Esegui JavaScript">
|
||||
]>
|
||||
<resources>
|
||||
<string name="about_app">&about_app;</string>
|
||||
<string name="about_app_menu">ℹ️ &about_app;</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="exit">&exit;</string>
|
||||
<string name="exit_menu">🚪 &exit;</string>
|
||||
<string name="hide">&hide;</string>
|
||||
<string name="hide_menu">🕳️ &hide;</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>
|
||||
<string name="loading">Caricamento</string>
|
||||
<string name="reload">&reload;</string>
|
||||
<string name="reload_menu">🔄 &reload;</string>
|
||||
<string name="stop_loading">&stop_loading;</string>
|
||||
<string name="stop_loading_menu">🛑 &stop_loading;</string>
|
||||
<string name="backwards">&backwards;</string>
|
||||
<string name="backwards_menu">⏮️ &backwards;</string>
|
||||
<string name="forward">&forward;</string>
|
||||
<string name="forward_menu">⏭️ &forward;</string>
|
||||
<string name="open">&open;</string>
|
||||
<string name="open_menu">👌 &open;</string>
|
||||
<string name="open_externally">&open_externally;</string>
|
||||
<string name="open_externally_menu">☝️ &open_externally;</string>
|
||||
<string name="copy_url">©_url;</string>
|
||||
<string name="copy_url_menu">🔗 ©_url;</string>
|
||||
<string name="execute_javascript">&execute_javascript;</string>
|
||||
<string name="execute_javascript_menu">🔣 &execute_javascript;</string>
|
||||
</resources>
|
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE resources [
|
||||
<!ENTITY about_app "About App">
|
||||
<!ENTITY exit "Exit">
|
||||
<!ENTITY hide "Hide">
|
||||
<!ENTITY reload "Reload">
|
||||
<!ENTITY stop_loading "Stop Loading">
|
||||
<!ENTITY backwards "Backwards">
|
||||
<!ENTITY forward "Forward">
|
||||
<!ENTITY open "Open">
|
||||
<!ENTITY open_externally "&open; Externally">
|
||||
<!ENTITY copy_url "Copy URL">
|
||||
<!ENTITY execute_javascript "Execute JavaScript">
|
||||
]>
|
||||
<resources>
|
||||
<string name="about_app">&about_app;</string>
|
||||
<string name="about_app_menu">ℹ️ &about_app;</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="exit">&exit;</string>
|
||||
<string name="exit_menu">🚪 &exit;</string>
|
||||
<string name="hide">&hide;</string>
|
||||
<string name="hide_menu">🕳️ &hide;</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>
|
||||
<string name="loading">Loading</string>
|
||||
<string name="reload">&reload;</string>
|
||||
<string name="reload_menu">🔄 &reload;</string>
|
||||
<string name="stop_loading">&stop_loading;</string>
|
||||
<string name="stop_loading_menu">🛑 &stop_loading;</string>
|
||||
<string name="backwards">&backwards;</string>
|
||||
<string name="backwards_menu">⏮️ &backwards;</string>
|
||||
<string name="forward">&forward;</string>
|
||||
<string name="forward_menu">⏭️ &forward;</string>
|
||||
<string name="open">&open;</string>
|
||||
<string name="open_menu">👌 &open;</string>
|
||||
<string name="open_externally">&open_externally;</string>
|
||||
<string name="open_externally_menu">☝️ &open_externally;</string>
|
||||
<string name="copy_url">©_url;</string>
|
||||
<string name="copy_url_menu">🔗 ©_url;</string>
|
||||
<string name="execute_javascript">&execute_javascript;</string>
|
||||
<string name="execute_javascript_menu">🔣 &execute_javascript;</string>
|
||||
</resources>
|
@@ -0,0 +1,17 @@
|
||||
package org.eu.spacc.spaccwebview.android;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user