diff --git a/app/build.gradle b/app/build.gradle
index 6a891ce..bdb468d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,6 +4,7 @@ plugins {
dependencies {
implementation 'androidx.core:core:1.3.0'
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}
android {
@@ -13,8 +14,8 @@ android {
applicationId "org.purplei2p.i2pd"
targetSdkVersion 29
minSdkVersion 14
- versionCode 23300
- versionName "2.33.0-67-gb7ebb3ea"
+ versionCode 23500
+ versionName "2.35.0-113-g01df1647"
setProperty("archivesBaseName", archivesBaseName + "-" + versionName)
ndk {
@@ -73,7 +74,7 @@ android {
}
}
-ext.abiCodes = ['armeabi-v7a':1, 'x86':2, 'arm64-v8a':3, 'x86_64':4]
+ext.abiCodes = ['armeabi-v7a': 1, 'x86': 2, 'arm64-v8a': 3, 'x86_64': 4]
import com.android.build.OutputFile
android.applicationVariants.all { variant ->
diff --git a/app/jni/i2pd b/app/jni/i2pd
index b7ebb3e..01df164 160000
--- a/app/jni/i2pd
+++ b/app/jni/i2pd
@@ -1 +1 @@
-Subproject commit b7ebb3ea3d1b275e567c4cd015749355911f3f60
+Subproject commit 01df1647bcfe7a9b92a90b457fdf2683e338a4f9
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3d18eae..097dfdf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,10 +15,15 @@
android:allowBackup="true"
android:icon="@drawable/icon"
android:label="@string/app_name"
- android:theme="@android:style/Theme.Holo.Light.DarkActionBar"
android:requestLegacyExternalStorage="true"
- android:usesCleartextTraffic="true"
- >
+ android:theme="@android:style/Theme.Holo.Light.DarkActionBar"
+ android:usesCleartextTraffic="true">
+
+
+
+
stateUpdateListeners = new HashSet<>();
-
- public static DaemonSingleton getInstance() {
- return instance;
- }
-
- public synchronized void addStateChangeListener(StateUpdateListener listener) {
- stateUpdateListeners.add(listener);
- }
-
- public synchronized void removeStateChangeListener(StateUpdateListener listener) {
- stateUpdateListeners.remove(listener);
- }
-
- private synchronized void setState(State newState) {
- if (newState == null)
- throw new NullPointerException();
-
- State oldState = state;
-
- if (oldState == null)
- throw new NullPointerException();
-
- if (oldState.equals(newState))
- return;
-
- state = newState;
- fireStateUpdate1();
- }
-
- public synchronized void stopAcceptingTunnels() {
- if (isStartedOkay()) {
- setState(State.gracefulShutdownInProgress);
- I2PD_JNI.stopAcceptingTunnels();
- }
- }
-
- public synchronized void startAcceptingTunnels() {
- if (isStartedOkay()) {
- setState(State.startedOkay);
- I2PD_JNI.startAcceptingTunnels();
- }
- }
-
- public synchronized void reloadTunnelsConfigs() {
- if (isStartedOkay()) {
- I2PD_JNI.reloadTunnelsConfigs();
- }
- }
-
- public synchronized int GetTransitTunnelsCount() {
- return I2PD_JNI.GetTransitTunnelsCount();
- }
-
- private volatile boolean startedOkay;
-
- public enum State {
- uninitialized(R.string.uninitialized),
- starting(R.string.starting),
- jniLibraryLoaded(R.string.jniLibraryLoaded),
- startedOkay(R.string.startedOkay),
- startFailed(R.string.startFailed),
- gracefulShutdownInProgress(R.string.gracefulShutdownInProgress),
- stopped(R.string.stopped);
-
- State(int statusStringResourceId) {
- this.statusStringResourceId = statusStringResourceId;
- }
-
- private final int statusStringResourceId;
-
- public int getStatusStringResourceId() {
- return statusStringResourceId;
- }
- }
-
- ;
-
- private volatile State state = State.uninitialized;
-
- public State getState() {
- return state;
- }
-
- {
- setState(State.starting);
- new Thread(new Runnable() {
-
- @Override
- public void run() {
- try {
- I2PD_JNI.loadLibraries();
- setState(State.jniLibraryLoaded);
- } catch (Throwable tr) {
- lastThrowable = tr;
- setState(State.startFailed);
- return;
- }
- try {
- synchronized (DaemonSingleton.this) {
- I2PD_JNI.setDataDir(Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd");
- daemonStartResult = I2PD_JNI.startDaemon();
- if ("ok".equals(daemonStartResult)) {
- setState(State.startedOkay);
- setStartedOkay(true);
- } else
- setState(State.startFailed);
- }
- } catch (Throwable tr) {
- lastThrowable = tr;
- setState(State.startFailed);
- }
- }
-
- }, "i2pdDaemonStart").start();
- }
-
- private Throwable lastThrowable;
- private String daemonStartResult = "N/A";
-
- private void fireStateUpdate1() {
- Log.i(TAG, "daemon state change: " + state);
- for (StateUpdateListener listener : stateUpdateListeners) {
- try {
- listener.daemonStateUpdate();
- } catch (Throwable tr) {
- Log.e(TAG, "exception in listener ignored", tr);
- }
- }
- }
-
- public Throwable getLastThrowable() {
- return lastThrowable;
- }
-
- public String getDaemonStartResult() {
- return daemonStartResult;
- }
-
- private final Object startedOkayLock = new Object();
-
- public boolean isStartedOkay() {
- synchronized (startedOkayLock) {
- return startedOkay;
- }
- }
-
- private void setStartedOkay(boolean startedOkay) {
- synchronized (startedOkayLock) {
- this.startedOkay = startedOkay;
- }
- }
-
- public synchronized void stopDaemon() {
- if (isStartedOkay()) {
- try {
- I2PD_JNI.stopDaemon();
- } catch (Throwable tr) {
- Log.e(TAG, "", tr);
- }
-
- setStartedOkay(false);
- setState(State.stopped);
- }
- }
-}
diff --git a/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java
new file mode 100644
index 0000000..5d39cb5
--- /dev/null
+++ b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java
@@ -0,0 +1,374 @@
+package org.purplei2p.i2pd;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import android.annotation.TargetApi;
+import android.content.res.AssetManager;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+public class DaemonWrapper {
+ private static final String TAG = "i2pd";
+ private final AssetManager assetManager;
+ private final ConnectivityManager connectivityManager;
+ private String i2pdpath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd/";
+ private boolean assetsCopied;
+
+ public interface StateUpdateListener {
+ void daemonStateUpdate(State oldValue, State newValue);
+ }
+
+ private final Set stateUpdateListeners = new HashSet<>();
+
+ public synchronized void addStateChangeListener(StateUpdateListener listener) {
+ stateUpdateListeners.add(listener);
+ }
+
+ public synchronized void removeStateChangeListener(StateUpdateListener listener) {
+ stateUpdateListeners.remove(listener);
+ }
+
+ private synchronized void setState(State newState) {
+ if (newState == null)
+ throw new NullPointerException();
+
+ State oldState = state;
+
+ if (oldState == null)
+ throw new NullPointerException();
+
+ if (oldState.equals(newState))
+ return;
+
+ state = newState;
+ fireStateUpdate1(oldState, newState);
+ }
+
+ public synchronized void stopAcceptingTunnels() {
+ if (isStartedOkay()) {
+ setState(State.gracefulShutdownInProgress);
+ I2PD_JNI.stopAcceptingTunnels();
+ }
+ }
+
+ public synchronized void startAcceptingTunnels() {
+ if (isStartedOkay()) {
+ setState(State.startedOkay);
+ I2PD_JNI.startAcceptingTunnels();
+ }
+ }
+
+ public synchronized void reloadTunnelsConfigs() {
+ if (isStartedOkay()) {
+ I2PD_JNI.reloadTunnelsConfigs();
+ }
+ }
+
+ public int getTransitTunnelsCount() {
+ return I2PD_JNI.GetTransitTunnelsCount();
+ }
+
+ public enum State {
+ uninitialized(R.string.uninitialized),
+ starting(R.string.starting),
+ jniLibraryLoaded(R.string.jniLibraryLoaded),
+ startedOkay(R.string.startedOkay),
+ startFailed(R.string.startFailed),
+ gracefulShutdownInProgress(R.string.gracefulShutdownInProgress),
+ stopped(R.string.stopped);
+
+ State(int statusStringResourceId) {
+ this.statusStringResourceId = statusStringResourceId;
+ }
+
+ private final int statusStringResourceId;
+
+ public int getStatusStringResourceId() {
+ return statusStringResourceId;
+ }
+
+ public boolean isStartedOkay() {
+ return equals(State.startedOkay) || equals(State.gracefulShutdownInProgress);
+ }
+ }
+
+ private volatile State state = State.uninitialized;
+
+ public State getState() {
+ return state;
+ }
+
+ public DaemonWrapper(AssetManager assetManager, ConnectivityManager connectivityManager){
+ this.assetManager = assetManager;
+ this.connectivityManager = connectivityManager;
+ setState(State.starting);
+ new Thread(() -> {
+ try {
+ processAssets();
+ I2PD_JNI.loadLibraries();
+ setState(State.jniLibraryLoaded);
+ registerNetworkCallback();
+ } catch (Throwable tr) {
+ lastThrowable = tr;
+ setState(State.startFailed);
+ return;
+ }
+ try {
+ synchronized (DaemonWrapper.this) {
+ I2PD_JNI.setDataDir(Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd");
+ daemonStartResult = I2PD_JNI.startDaemon();
+ if ("ok".equals(daemonStartResult)) {
+ setState(State.startedOkay);
+ } else
+ setState(State.startFailed);
+ }
+ } catch (Throwable tr) {
+ lastThrowable = tr;
+ setState(State.startFailed);
+ }
+ }, "i2pdDaemonStart").start();
+ }
+
+ private Throwable lastThrowable;
+ private String daemonStartResult = "N/A";
+
+ private void fireStateUpdate1(State oldValue, State newValue) {
+ Log.i(TAG, "daemon state change: " + state);
+ for (StateUpdateListener listener : stateUpdateListeners) {
+ try {
+ listener.daemonStateUpdate(oldValue, newValue);
+ } catch (Throwable tr) {
+ Log.e(TAG, "exception in listener ignored", tr);
+ }
+ }
+ }
+
+ public Throwable getLastThrowable() {
+ return lastThrowable;
+ }
+
+ public String getDaemonStartResult() {
+ return daemonStartResult;
+ }
+
+ public boolean isStartedOkay() {
+ return getState().isStartedOkay();
+ }
+
+ public synchronized void stopDaemon() {
+ if (isStartedOkay()) {
+ try {
+ I2PD_JNI.stopDaemon();
+ } catch (Throwable tr) {
+ Log.e(TAG, "", tr);
+ }
+
+ setState(State.stopped);
+ }
+ }
+
+ private void processAssets() {
+ if (!assetsCopied) {
+ try {
+ assetsCopied = true;
+
+ File holderFile = new File(i2pdpath, "assets.ready");
+ String versionName = BuildConfig.VERSION_NAME; // here will be app version, like 2.XX.XX
+ StringBuilder text = new StringBuilder();
+
+ if (holderFile.exists()) {
+ try { // if holder file exists, read assets version string
+ FileReader fileReader = new FileReader(holderFile);
+
+ try {
+ BufferedReader br = new BufferedReader(fileReader);
+
+ try {
+ String line;
+
+ while ((line = br.readLine()) != null) {
+ text.append(line);
+ }
+ }finally {
+ try {
+ br.close();
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ } finally {
+ try {
+ fileReader.close();
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ // if version differs from current app version or null, try to delete certificates folder
+ if (!text.toString().contains(versionName))
+ try {
+ boolean deleteResult = holderFile.delete();
+ if (!deleteResult)
+ Log.e(TAG, "holderFile.delete() returned " + deleteResult + ", absolute path='" + holderFile.getAbsolutePath() + "'");
+ File certPath = new File(i2pdpath, "certificates");
+ deleteRecursive(certPath);
+ }
+ catch (Throwable tr) {
+ Log.e(TAG, "", tr);
+ }
+
+ // copy assets. If processed file exists, it won't be overwritten
+ copyAsset("addressbook");
+ copyAsset("certificates");
+ copyAsset("tunnels.d");
+ copyAsset("i2pd.conf");
+ copyAsset("subscriptions.txt");
+ copyAsset("tunnels.conf");
+
+ // update holder file about successful copying
+ FileWriter writer = new FileWriter(holderFile);
+ try {
+ writer.append(versionName);
+ } finally {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ Log.e(TAG,"on writer close", e);
+ }
+ }
+ }
+ catch (Throwable tr)
+ {
+ Log.e(TAG,"on assets copying", tr);
+ }
+ }
+ }
+
+ /**
+ * Copy the asset at the specified path to this app's data directory. If the
+ * asset is a directory, its contents are also copied.
+ *
+ * @param path
+ * Path to asset, relative to app's assets directory.
+ */
+ private void copyAsset(String path) {
+ // If we have a directory, we make it and recurse. If a file, we copy its
+ // contents.
+ try {
+ String[] contents = assetManager.list(path);
+
+ // The documentation suggests that list throws an IOException, but doesn't
+ // say under what conditions. It'd be nice if it did so when the path was
+ // to a file. That doesn't appear to be the case. If the returned array is
+ // null or has 0 length, we assume the path is to a file. This means empty
+ // directories will get turned into files.
+ if (contents == null || contents.length == 0) {
+ copyFileAsset(path);
+ return;
+ }
+
+ // Make the directory.
+ File dir = new File(i2pdpath, path);
+ boolean result = dir.mkdirs();
+ Log.d(TAG, "dir.mkdirs() returned " + result);
+
+ // Recurse on the contents.
+ for (String entry : contents) {
+ copyAsset(path + '/' + entry);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "ex ignored for path='" + path + "'", e);
+ }
+ }
+
+ /**
+ * Copy the asset file specified by path to app's data directory. Assumes
+ * parent directories have already been created.
+ *
+ * @param path
+ * Path to asset, relative to app's assets directory.
+ */
+ private void copyFileAsset(String path) {
+ File file = new File(i2pdpath, path);
+ if (!file.exists()) {
+ try {
+ try (InputStream in = assetManager.open(path)) {
+ try (OutputStream out = new FileOutputStream(file)) {
+ byte[] buffer = new byte[1024];
+ int read = in.read(buffer);
+ while (read != -1) {
+ out.write(buffer, 0, read);
+ read = in.read(buffer);
+ }
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+
+ private void deleteRecursive(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ File[] files = fileOrDirectory.listFiles();
+ if (files != null) {
+ for (File child : files) {
+ deleteRecursive(child);
+ }
+ }
+ }
+ boolean deleteResult = fileOrDirectory.delete();
+ if (!deleteResult)
+ Log.e(TAG, "fileOrDirectory.delete() returned " + deleteResult + ", absolute path='" + fileOrDirectory.getAbsolutePath() + "'");
+ }
+
+ private void registerNetworkCallback(){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) registerNetworkCallback0();
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ private void registerNetworkCallback0() {
+ NetworkRequest request = new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ .build();
+ NetworkStateCallbackImpl networkCallback = new NetworkStateCallbackImpl();
+ connectivityManager.registerNetworkCallback(request, networkCallback);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ private static final class NetworkStateCallbackImpl extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(Network network) {
+ super.onAvailable(network);
+ I2PD_JNI.onNetworkStateChanged(true);
+ Log.i(TAG, "NetworkCallback.onAvailable");
+ }
+
+ @Override
+ public void onLost(Network network) {
+ super.onLost(network);
+ I2PD_JNI.onNetworkStateChanged(false);
+ Log.i(TAG, " NetworkCallback.onLost");
+ }
+ }
+}
diff --git a/app/src/main/java/org/purplei2p/i2pd/ForegroundService.java b/app/src/main/java/org/purplei2p/i2pd/ForegroundService.java
index a048711..8f24280 100644
--- a/app/src/main/java/org/purplei2p/i2pd/ForegroundService.java
+++ b/app/src/main/java/org/purplei2p/i2pd/ForegroundService.java
@@ -10,39 +10,43 @@ import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
-
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
-
import android.util.Log;
public class ForegroundService extends Service {
private static final String TAG = "FgService";
-
private volatile boolean shown;
+ private static ForegroundService instance;
+ private static volatile DaemonWrapper daemon;
+ private static final Object initDeinitLock = new Object();
- private final DaemonSingleton.StateUpdateListener daemonStateUpdatedListener =
- new DaemonSingleton.StateUpdateListener() {
+ private final DaemonWrapper.StateUpdateListener daemonStateUpdatedListener =
+ new DaemonWrapper.StateUpdateListener() {
@Override
- public void daemonStateUpdate() {
- try {
- synchronized (ForegroundService.this) {
- if (shown) cancelNotification();
- showNotification();
- }
- } catch (Throwable tr) {
- Log.e(TAG, "error ignored", tr);
- }
+ public void daemonStateUpdate(DaemonWrapper.State oldValue, DaemonWrapper.State newValue) {
+ updateNotificationText();
}
};
+ private void updateNotificationText() {
+ try {
+ synchronized (initDeinitLock) {
+ if (shown) cancelNotification();
+ showNotification();
+ }
+ } catch (Throwable tr) {
+ Log.e(TAG,"error ignored",tr);
+ }
+ }
+
private NotificationManager notificationManager;
// Unique Identification Number for the Notification.
// We use it on Notification start, and to cancel it.
- private int NOTIFICATION = 1;
+ private static final int NOTIFICATION = 1;
/**
* Class for clients to access. Because we know this service always
@@ -55,16 +59,27 @@ public class ForegroundService extends Service {
}
}
+ public static void init(DaemonWrapper daemon) {
+ ForegroundService.daemon = daemon;
+ initCheck();
+ }
+
+ private static void initCheck() {
+ synchronized (initDeinitLock) {
+ if (instance != null && daemon != null) instance.setListener();
+ }
+ }
+
@Override
public void onCreate() {
- notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
+ instance = this;
+ initCheck();
+ }
- synchronized (this) {
- DaemonSingleton.getInstance().addStateChangeListener(daemonStateUpdatedListener);
- if (!shown) daemonStateUpdatedListener.daemonStateUpdate();
- }
- // Tell the user we started.
-// Toast.makeText(this, R.string.i2pd_service_started, Toast.LENGTH_SHORT).show();
+ private void setListener() {
+ daemon.addStateChangeListener(daemonStateUpdatedListener);
+ updateNotificationText();
}
@Override
@@ -75,19 +90,33 @@ public class ForegroundService extends Service {
@Override
public void onDestroy() {
- DaemonSingleton.getInstance().removeStateChangeListener(daemonStateUpdatedListener);
cancelNotification();
+ deinitCheck();
+ instance=null;
+ }
+
+ public static void deinit() {
+ deinitCheck();
+ }
+
+ private static void deinitCheck() {
+ synchronized (initDeinitLock) {
+ if (daemon != null && instance != null)
+ daemon.removeStateChangeListener(instance.daemonStateUpdatedListener);
+ }
}
- private synchronized void cancelNotification() {
- // Cancel the persistent notification.
- notificationManager.cancel(NOTIFICATION);
+ private void cancelNotification() {
+ synchronized (initDeinitLock) {
+ // Cancel the persistent notification.
+ notificationManager.cancel(NOTIFICATION);
- stopForeground(true);
+ stopForeground(true);
- // Tell the user we stopped.
- //Toast.makeText(this, R.string.i2pd_service_stopped, Toast.LENGTH_SHORT).show();
- shown = false;
+ // Tell the user we stopped.
+ //Toast.makeText(this, R.string.i2pd_service_stopped, Toast.LENGTH_SHORT).show();
+ shown = false;
+ }
}
@Override
@@ -102,38 +131,42 @@ public class ForegroundService extends Service {
/**
* Show a notification while this service is running.
*/
- private synchronized void showNotification() {
- // In this sample, we'll use the same text for the ticker and the expanded notification
- CharSequence text = getText(DaemonSingleton.getInstance().getState().getStatusStringResourceId());
-
- // The PendingIntent to launch our activity if the user selects this notification
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
- new Intent(this, I2PDActivity.class), 0);
-
- // If earlier version channel ID is not used
- // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
- String channelId = Build.VERSION.SDK_INT >= 26 ? createNotificationChannel() : "";
-
- // Set the info for the views that show in the notification panel.
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId)
- .setOngoing(true)
- .setSmallIcon(R.drawable.itoopie_notification_icon); // the status icon
- if (Build.VERSION.SDK_INT >= 16)
- builder = builder.setPriority(Notification.PRIORITY_DEFAULT);
- if (Build.VERSION.SDK_INT >= 21)
- builder = builder.setCategory(Notification.CATEGORY_SERVICE);
- Notification notification = builder
- .setTicker(text) // the status text
- .setWhen(System.currentTimeMillis()) // the time stamp
- .setContentTitle(getText(R.string.app_name)) // the label of the entry
- .setContentText(text) // the contents of the entry
- .setContentIntent(contentIntent) // The intent to send when the entry is clicked
- .build();
-
- // Send the notification.
- //mNM.notify(NOTIFICATION, notification);
- startForeground(NOTIFICATION, notification);
- shown = true;
+ private void showNotification() {
+ synchronized (initDeinitLock) {
+ if (daemon != null) {
+ // In this sample, we'll use the same text for the ticker and the expanded notification
+ CharSequence text = getText(daemon.getState().getStatusStringResourceId());
+
+ // The PendingIntent to launch our activity if the user selects this notification
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+ new Intent(this, I2PDActivity.class), 0);
+
+ // If earlier version channel ID is not used
+ // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
+ String channelId = Build.VERSION.SDK_INT >= 26 ? createNotificationChannel() : "";
+
+ // Set the info for the views that show in the notification panel.
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId)
+ .setOngoing(true)
+ .setSmallIcon(R.drawable.itoopie_notification_icon); // the status icon
+ if (Build.VERSION.SDK_INT >= 16)
+ builder = builder.setPriority(Notification.PRIORITY_DEFAULT);
+ if (Build.VERSION.SDK_INT >= 21)
+ builder = builder.setCategory(Notification.CATEGORY_SERVICE);
+ Notification notification = builder
+ .setTicker(text) // the status text
+ .setWhen(System.currentTimeMillis()) // the time stamp
+ .setContentTitle(getText(R.string.app_name)) // the label of the entry
+ .setContentText(text) // the contents of the entry
+ .setContentIntent(contentIntent) // The intent to send when the entry is clicked
+ .build();
+
+ // Send the notification.
+ //mNM.notify(NOTIFICATION, notification);
+ startForeground(NOTIFICATION, notification);
+ shown = true;
+ }
+ }
}
@RequiresApi(Build.VERSION_CODES.O)
@@ -148,6 +181,4 @@ public class ForegroundService extends Service {
else Log.e(TAG, "error: NOTIFICATION_SERVICE is null");
return channelId;
}
-
- private static final DaemonSingleton daemon = DaemonSingleton.getInstance();
}
diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java
index 0bc1f75..f9ddee3 100644
--- a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java
+++ b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java
@@ -1,13 +1,5 @@
package org.purplei2p.i2pd;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.BufferedReader;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Timer;
@@ -15,7 +7,6 @@ import java.util.TimerTask;
import android.Manifest;
import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
@@ -24,16 +15,11 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
-import android.content.res.AssetManager;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Bundle;
import android.os.Build;
-import android.os.Environment;
import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
@@ -44,9 +30,7 @@ import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
@@ -60,44 +44,42 @@ import android.webkit.WebViewClient;
import static android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS;
public class I2PDActivity extends Activity {
- private WebView webView;
-
private static final String TAG = "i2pdActvt";
private static final int MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
public static final int GRACEFUL_DELAY_MILLIS = 10 * 60 * 1000;
public static final String PACKAGE_URI_SCHEME = "package:";
private TextView textView;
- private boolean assetsCopied;
- private NetworkStateCallback networkCallback;
- private String i2pdpath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd/";
- //private ConfigParser parser = new ConfigParser(i2pdpath); // TODO:
- private static final DaemonSingleton daemon = DaemonSingleton.getInstance();
+ private static volatile DaemonWrapper daemon;
- private final DaemonSingleton.StateUpdateListener daemonStateUpdatedListener = new DaemonSingleton.StateUpdateListener() {
+ private final DaemonWrapper.StateUpdateListener daemonStateUpdatedListener = new DaemonWrapper.StateUpdateListener() {
@Override
- public void daemonStateUpdate() {
- processAssets();
- runOnUiThread(() -> {
- try {
- if (textView == null)
- return;
- Throwable tr = daemon.getLastThrowable();
- if (tr != null) {
- textView.setText(throwableToString(tr));
- return;
- }
- DaemonSingleton.State state = daemon.getState();
- String startResultStr = DaemonSingleton.State.startFailed.equals(state) ? String.format(": %s", daemon.getDaemonStartResult()) : "";
- String graceStr = DaemonSingleton.State.gracefulShutdownInProgress.equals(state) ? String.format(": %s %s", formatGraceTimeRemaining(), getText(R.string.remaining)) : "";
- textView.setText(String.format("%s%s%s", getText(state.getStatusStringResourceId()), startResultStr, graceStr));
- } catch (Throwable tr) {
- Log.e(TAG, "error ignored", tr);
- }
- });
+ public void daemonStateUpdate(DaemonWrapper.State oldValue, DaemonWrapper.State newValue) {
+ updateStatusText();
}
};
+
+ private void updateStatusText() {
+ runOnUiThread(() -> {
+ try {
+ if (textView == null)
+ return;
+ Throwable tr = daemon.getLastThrowable();
+ if (tr!=null) {
+ textView.setText(throwableToString(tr));
+ return;
+ }
+ DaemonWrapper.State state = daemon.getState();
+ String startResultStr = DaemonWrapper.State.startFailed.equals(state) ? String.format(": %s", daemon.getDaemonStartResult()) : "";
+ String graceStr = DaemonWrapper.State.gracefulShutdownInProgress.equals(state) ? String.format(": %s %s", formatGraceTimeRemaining(), getText(R.string.remaining)) : "";
+ textView.setText(String.format("%s%s%s", getText(state.getStatusStringResourceId()), startResultStr, graceStr));
+ } catch (Throwable tr) {
+ Log.e(TAG,"error ignored",tr);
+ }
+ });
+ }
+
private static volatile long graceStartedMillis;
private static final Object graceStartedMillis_LOCK = new Object();
private Menu optionsMenu;
@@ -117,17 +99,23 @@ public class I2PDActivity extends Activity {
Log.i(TAG, "onCreate");
super.onCreate(savedInstanceState);
+ if (daemon==null) {
+ ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+ daemon = new DaemonWrapper(getAssets(), connectivityManager);
+ }
+ ForegroundService.init(daemon);
+
textView = new TextView(this);
setContentView(textView);
daemon.addStateChangeListener(daemonStateUpdatedListener);
- daemonStateUpdatedListener.daemonStateUpdate();
+ daemonStateUpdatedListener.daemonStateUpdate(DaemonWrapper.State.uninitialized, daemon.getState());
// request permissions
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
- new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
- MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE);
}
}
@@ -144,16 +132,13 @@ public class I2PDActivity extends Activity {
}
openBatteryOptimizationDialogIfNeeded();
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- registerNetworkCallback();
- }
}
@Override
protected void onDestroy() {
super.onDestroy();
textView = null;
+ ForegroundService.deinit();
daemon.removeStateChangeListener(daemonStateUpdatedListener);
//cancelGracefulStop0();
try {
@@ -164,7 +149,8 @@ public class I2PDActivity extends Activity {
}
@Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
+ {
if (requestCode == MY_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
Log.e(TAG, "WR_EXT_STORAGE perm granted");
@@ -196,26 +182,26 @@ public class I2PDActivity extends Activity {
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
- // This is called when the connection with the service has been
- // established, giving us the service object we can use to
- // interact with the service. Because we have bound to a explicit
- // service that we know is running in our own process, we can
- // cast its IBinder to a concrete class and directly access it.
- // mBoundService = ((LocalService.LocalBinder)service).getService();
-
- // Tell the user about this for our demo.
- // Toast.makeText(Binding.this, R.string.local_service_connected,
- // Toast.LENGTH_SHORT).show();
+ /* This is called when the connection with the service has been
+ established, giving us the service object we can use to
+ interact with the service. Because we have bound to a explicit
+ service that we know is running in our own process, we can
+ cast its IBinder to a concrete class and directly access it. */
+ // mBoundService = ((LocalService.LocalBinder)service).getService();
+
+ /* Tell the user about this for our demo. */
+ // Toast.makeText(Binding.this, R.string.local_service_connected,
+ // Toast.LENGTH_SHORT).show();
}
public void onServiceDisconnected(ComponentName className) {
- // This is called when the connection with the service has been
- // unexpectedly disconnected -- that is, its process crashed.
- // Because it is running in our same process, we should never
- // see this happen.
- // mBoundService = null;
- // Toast.makeText(Binding.this, R.string.local_service_disconnected,
- // Toast.LENGTH_SHORT).show();
+ /* This is called when the connection with the service has been
+ unexpectedly disconnected -- that is, its process crashed.
+ Because it is running in our same process, we should never
+ see this happen. */
+ // mBoundService = null;
+ // Toast.makeText(Binding.this, R.string.local_service_disconnected,
+ // Toast.LENGTH_SHORT).show();
}
};
@@ -287,15 +273,8 @@ public class I2PDActivity extends Activity {
return true;
case R.id.action_start_webview:
- setContentView(R.layout.webview);
- this.webView = (WebView) findViewById(R.id.webview1);
- this.webView.setWebViewClient(new WebViewClient());
-
- WebSettings webSettings = this.webView.getSettings();
- webSettings.setBuiltInZoomControls(true);
- webSettings.setJavaScriptEnabled(false);
- this.webView.loadUrl("http://127.0.0.1:7070"); // TODO: instead 7070 I2Pd....HttpPort
- break;
+ startActivity(new Intent(getApplicationContext(), WebConsoleActivity.class));
+ return true;
}
return super.onOptionsItemSelected(item);
@@ -334,7 +313,7 @@ public class I2PDActivity extends Activity {
private static volatile Timer gracefulQuitTimer;
private void i2pdGracefulStop() {
- if (daemon.getState() == DaemonSingleton.State.stopped) {
+ if (daemon.getState() == DaemonWrapper.State.stopped) {
Toast.makeText(this, R.string.already_stopped, Toast.LENGTH_SHORT).show();
return;
}
@@ -362,7 +341,8 @@ public class I2PDActivity extends Activity {
}, "gracInit").start();
}
- private void cancelGracefulStop() {
+ private void cancelGracefulStop()
+ {
cancelGracefulStop0();
new Thread(() -> {
try {
@@ -382,9 +362,10 @@ public class I2PDActivity extends Activity {
if (gracefulQuitTimerOld != null)
gracefulQuitTimerOld.cancel();
- if (daemon.GetTransitTunnelsCount() <= 0) { // no tunnels left
+ if (daemon.getTransitTunnelsCount() <= 0) { // no tunnels left
Log.d(TAG, "no transit tunnels left, stopping");
i2pdStop();
+ return;
}
final Timer gracefulQuitTimer = new Timer(true);
@@ -400,7 +381,7 @@ public class I2PDActivity extends Activity {
final TimerTask tickerTask = new TimerTask() {
@Override
public void run() {
- daemonStateUpdatedListener.daemonStateUpdate();
+ updateStatusText();
}
};
gracefulQuitTimer.scheduleAtFixedRate(tickerTask, 0/*start delay*/, 1000/*millis period*/);
@@ -425,162 +406,6 @@ public class I2PDActivity extends Activity {
});
}
- /**
- * Copy the asset at the specified path to this app's data directory. If the
- * asset is a directory, its contents are also copied.
- *
- * @param path Path to asset, relative to app's assets directory.
- */
- private void copyAsset(String path) {
- AssetManager manager = getAssets();
-
- // If we have a directory, we make it and recurse. If a file, we copy its
- // contents.
- try {
- String[] contents = manager.list(path);
-
- // The documentation suggests that list throws an IOException, but doesn't
- // say under what conditions. It'd be nice if it did so when the path was
- // to a file. That doesn't appear to be the case. If the returned array is
- // null or has 0 length, we assume the path is to a file. This means empty
- // directories will get turned into files.
- if (contents == null || contents.length == 0) {
- copyFileAsset(path);
- return;
- }
-
- // Make the directory.
- File dir = new File(i2pdpath, path);
- boolean result = dir.mkdirs();
- Log.d(TAG, "dir.mkdirs() returned " + result);
-
- // Recurse on the contents.
- for (String entry : contents) {
- copyAsset(path + '/' + entry);
- }
- } catch (IOException e) {
- Log.e(TAG, "ex ignored for path='" + path + "'", e);
- }
- }
-
- /**
- * Copy the asset file specified by path to app's data directory. Assumes
- * parent directories have already been created.
- *
- * @param path Path to asset, relative to app's assets directory.
- */
- private void copyFileAsset(String path) {
- File file = new File(i2pdpath, path);
- if (!file.exists()) {
- try {
- try (InputStream in = getAssets().open(path)) {
- try (OutputStream out = new FileOutputStream(file)) {
- byte[] buffer = new byte[1024];
- int read = in.read(buffer);
- while (read != -1) {
- out.write(buffer, 0, read);
- read = in.read(buffer);
- }
- }
- }
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- }
- }
-
- private void deleteRecursive(File fileOrDirectory) {
- if (fileOrDirectory.isDirectory()) {
- File[] files = fileOrDirectory.listFiles();
- if (files != null) {
- for (File child : files) {
- deleteRecursive(child);
- }
- }
- }
- boolean deleteResult = fileOrDirectory.delete();
- if (!deleteResult)
- Log.e(TAG, "fileOrDirectory.delete() returned " + deleteResult + ", absolute path='" + fileOrDirectory.getAbsolutePath() + "'");
- }
-
- private void processAssets() {
- if (!assetsCopied) {
- try {
- assetsCopied = true; // prevent from running on every state update
-
- File holderFile = new File(i2pdpath, "assets.ready");
- String versionName = BuildConfig.VERSION_NAME; // here will be app version, like 2.XX.XX
- StringBuilder text = new StringBuilder();
-
- if (holderFile.exists()) {
- try { // if holder file exists, read assets version string
- FileReader fileReader = new FileReader(holderFile);
-
- try {
- BufferedReader br = new BufferedReader(fileReader);
-
- try {
- String line;
-
- while ((line = br.readLine()) != null) {
- text.append(line);
- }
- } finally {
- try {
- br.close();
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- }
- } finally {
- try {
- fileReader.close();
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- }
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- }
-
- // if version differs from current app version or null, try to delete certificates folder
- if (!text.toString().contains(versionName))
- try {
- boolean deleteResult = holderFile.delete();
- if (!deleteResult)
- Log.e(TAG, "holderFile.delete() returned " + deleteResult + ", absolute path='" + holderFile.getAbsolutePath() + "'");
- File certPath = new File(i2pdpath, "certificates");
- deleteRecursive(certPath);
- } catch (Throwable tr) {
- Log.e(TAG, "", tr);
- }
-
- // copy assets. If processed file exists, it won't be overwritten
- copyAsset("addressbook");
- copyAsset("certificates");
- copyAsset("tunnels.d");
- copyAsset("i2pd.conf");
- copyAsset("subscriptions.txt");
- copyAsset("tunnels.conf");
-
- // update holder file about successful copying
- FileWriter writer = new FileWriter(holderFile);
- try {
- writer.append(versionName);
- } finally {
- try {
- writer.close();
- } catch (IOException e) {
- Log.e(TAG, "on writer close", e);
- }
- }
- } catch (Throwable tr) {
- Log.e(TAG, "on assets copying", tr);
- }
- }
- }
-
@SuppressLint("BatteryLife")
private void openBatteryOptimizationDialogIfNeeded() {
boolean questionEnabled = getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true);
@@ -621,7 +446,7 @@ public class I2PDActivity extends Activity {
Log.i(TAG, "BATT_OPTIM: ignoring==" + ignoring);
return ignoring;
} else {
- Log.i(TAG, "BATT_OPTIM: old sdk version==" + Build.VERSION.SDK_INT);
+ Log.i(TAG, "BATT_OPTIM: old SDK version==" + Build.VERSION.SDK_INT);
return false;
}
}
@@ -635,33 +460,6 @@ public class I2PDActivity extends Activity {
return "show_battery_optimization" + (device == null ? "" : device);
}
- @TargetApi(Build.VERSION_CODES.M)
- private void registerNetworkCallback() {
- ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkRequest request = new NetworkRequest.Builder()
- .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
- .build();
- networkCallback = new NetworkStateCallback();
- connectivityManager.registerNetworkCallback(request, networkCallback);
- }
-
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- private final class NetworkStateCallback extends ConnectivityManager.NetworkCallback {
- @Override
- public void onAvailable(Network network) {
- super.onAvailable(network);
- I2PD_JNI.onNetworkStateChanged(true);
- Log.i(TAG, "NetworkCallback.onAvailable");
- }
-
- @Override
- public void onLost(Network network) {
- super.onLost(network);
- I2PD_JNI.onNetworkStateChanged(false);
- Log.i(TAG, " NetworkCallback.onLost");
- }
- }
-
private void quit() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -669,7 +467,7 @@ public class I2PDActivity extends Activity {
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
finishAffinity();
} else {
- //moveTaskToBack(true);
+ // moveTaskToBack(true);
finish();
}
} catch (Throwable tr) {
diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PDPermsAskerActivity.java b/app/src/main/java/org/purplei2p/i2pd/I2PDPermsAskerActivity.java
index dd54e79..b178e10 100644
--- a/app/src/main/java/org/purplei2p/i2pd/I2PDPermsAskerActivity.java
+++ b/app/src/main/java/org/purplei2p/i2pd/I2PDPermsAskerActivity.java
@@ -103,7 +103,7 @@ public class I2PDPermsAskerActivity extends Activity {
@Override
public void onRequestPermissionsResult(int requestCode,
- String permissions[], int[] grantResults) {
+ String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSION_WRITE_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
diff --git a/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java b/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java
index 7699677..c4ff623 100644
--- a/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java
+++ b/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java
@@ -11,17 +11,13 @@ public class NetworkStateChangeReceiver extends BroadcastReceiver {
private static final String TAG = "i2pd";
- //api level 1
@Override
public void onReceive(final Context context, final Intent intent) {
Log.d(TAG, "Network state change");
try {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- assert cm != null;
NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo();
boolean isConnected = activeNetworkInfo != null && activeNetworkInfo.isConnected();
- // https://developer.android.com/training/monitoring-device-state/connectivity-monitoring.html?hl=ru
- // boolean isWiFi = activeNetworkInfo!=null && (activeNetworkInfo.getType() == ConnectivityManager.TYPE_WIFI);
I2PD_JNI.onNetworkStateChanged(isConnected);
} catch (Throwable tr) {
diff --git a/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java b/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java
new file mode 100644
index 0000000..5e20e8f
--- /dev/null
+++ b/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java
@@ -0,0 +1,67 @@
+package org.purplei2p.i2pd;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.MenuItem;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import java.util.Objects;
+
+public class WebConsoleActivity extends Activity {
+ private WebView webView;
+ private SwipeRefreshLayout swipeRefreshLayout;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_web_console);
+
+ Objects.requireNonNull(getActionBar()).setDisplayHomeAsUpEnabled(true);
+
+ webView = (WebView) findViewById(R.id.webview1);
+ webView.setWebViewClient(new WebViewClient());
+
+ final WebSettings webSettings = webView.getSettings();
+ webSettings.setBuiltInZoomControls(true);
+ webSettings.setJavaScriptEnabled(false);
+ webView.loadUrl("http://127.0.0.1:7070"); // TODO: instead 7070 I2Pd....HttpPort
+
+ swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe);
+ swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ swipeRefreshLayout.setRefreshing(true);
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ swipeRefreshLayout.setRefreshing(false);
+ webView.reload();
+ }
+ }, 1000);
+ }
+ });
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (webView.canGoBack()) {
+ webView.goBack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/res/layout/activity_web_console.xml b/app/src/main/res/layout/activity_web_console.xml
new file mode 100644
index 0000000..60d22a0
--- /dev/null
+++ b/app/src/main/res/layout/activity_web_console.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/webview.xml b/app/src/main/res/layout/webview.xml
deleted file mode 100644
index 887896a..0000000
--- a/app/src/main/res/layout/webview.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-