From aaecf06379b2447ba3754265668d5bc6994b72c3 Mon Sep 17 00:00:00 2001 From: R4SAS Date: Wed, 10 Feb 2021 00:19:33 +0300 Subject: [PATCH] pull changes from main repository Signed-off-by: R4SAS --- app/build.gradle | 7 +- app/jni/i2pd | 2 +- app/src/main/AndroidManifest.xml | 11 +- app/src/main/assets/addressbook/addresses.csv | 711 +++++------------- app/src/main/assets/certificates | 1 - .../assets/certificates/family/gostcoin.crt | 13 + .../assets/certificates/family/i2p-dev.crt | 13 + .../assets/certificates/family/i2pd-dev.crt | 13 + .../assets/certificates/family/mca2-i2p.crt | 12 + .../assets/certificates/family/volatile.crt | 12 + .../reseed/acetone_at_mail.i2p.crt | 32 + .../reseed/creativecowpat_at_mail.i2p.crt | 35 + .../reseed/echelon_at_mail.i2p.crt | 32 + .../reseed/hankhill19580_at_gmail.com.crt | 34 + .../reseed/hottuna_at_mail.i2p.crt | 33 + .../certificates/reseed/igor_at_novg.net.crt | 33 + .../reseed/lazygravy_at_mail.i2p.crt | 34 + .../reseed/r4sas-reseed_at_mail.i2p.crt | 32 + .../reseed/reseed_at_diva.exchange.crt | 34 + .../router/orignal_at_mail.i2p.crt | 31 + app/src/main/assets/i2pd.conf | 2 - app/src/main/assets/subscriptions.txt | 3 +- app/src/main/assets/tunnels.conf | 2 +- app/src/main/assets/tunnels.d | 1 - app/src/main/assets/tunnels.d/IRC-Ilita.conf | 7 + app/src/main/assets/tunnels.d/IRC-Irc2P.conf | 7 + app/src/main/assets/tunnels.d/README | 4 + .../org/purplei2p/i2pd/DaemonSingleton.java | 184 ----- .../org/purplei2p/i2pd/DaemonWrapper.java | 374 +++++++++ .../org/purplei2p/i2pd/ForegroundService.java | 159 ++-- .../java/org/purplei2p/i2pd/I2PDActivity.java | 330 ++------ .../i2pd/I2PDPermsAskerActivity.java | 2 +- .../i2pd/NetworkStateChangeReceiver.java | 4 - .../purplei2p/i2pd/WebConsoleActivity.java | 67 ++ .../main/res/layout/activity_web_console.xml | 22 + app/src/main/res/layout/webview.xml | 12 - 36 files changed, 1234 insertions(+), 1071 deletions(-) delete mode 120000 app/src/main/assets/certificates create mode 100644 app/src/main/assets/certificates/family/gostcoin.crt create mode 100644 app/src/main/assets/certificates/family/i2p-dev.crt create mode 100644 app/src/main/assets/certificates/family/i2pd-dev.crt create mode 100644 app/src/main/assets/certificates/family/mca2-i2p.crt create mode 100644 app/src/main/assets/certificates/family/volatile.crt create mode 100644 app/src/main/assets/certificates/reseed/acetone_at_mail.i2p.crt create mode 100644 app/src/main/assets/certificates/reseed/creativecowpat_at_mail.i2p.crt create mode 100644 app/src/main/assets/certificates/reseed/echelon_at_mail.i2p.crt create mode 100644 app/src/main/assets/certificates/reseed/hankhill19580_at_gmail.com.crt create mode 100644 app/src/main/assets/certificates/reseed/hottuna_at_mail.i2p.crt create mode 100644 app/src/main/assets/certificates/reseed/igor_at_novg.net.crt create mode 100644 app/src/main/assets/certificates/reseed/lazygravy_at_mail.i2p.crt create mode 100644 app/src/main/assets/certificates/reseed/r4sas-reseed_at_mail.i2p.crt create mode 100644 app/src/main/assets/certificates/reseed/reseed_at_diva.exchange.crt create mode 100644 app/src/main/assets/certificates/router/orignal_at_mail.i2p.crt delete mode 120000 app/src/main/assets/tunnels.d create mode 100644 app/src/main/assets/tunnels.d/IRC-Ilita.conf create mode 100644 app/src/main/assets/tunnels.d/IRC-Irc2P.conf create mode 100644 app/src/main/assets/tunnels.d/README delete mode 100644 app/src/main/java/org/purplei2p/i2pd/DaemonSingleton.java create mode 100644 app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java create mode 100644 app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java create mode 100644 app/src/main/res/layout/activity_web_console.xml delete mode 100644 app/src/main/res/layout/webview.xml 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 @@ - - - -