From b6175132eb84649dd98d84a8475d29a9cb9072e1 Mon Sep 17 00:00:00 2001 From: user Date: Fri, 23 Oct 2020 21:41:42 +0800 Subject: [PATCH 1/3] android: fix for pre-init jni calls; processAssets moved to a logical place --- .../org/purplei2p/i2pd/DaemonSingleton.java | 181 -------- .../src/org/purplei2p/i2pd/DaemonWrapper.java | 387 ++++++++++++++++++ .../org/purplei2p/i2pd/ForegroundService.java | 98 +++-- .../src/org/purplei2p/i2pd/I2PDActivity.java | 236 +---------- 4 files changed, 466 insertions(+), 436 deletions(-) delete mode 100644 android/src/org/purplei2p/i2pd/DaemonSingleton.java create mode 100644 android/src/org/purplei2p/i2pd/DaemonWrapper.java diff --git a/android/src/org/purplei2p/i2pd/DaemonSingleton.java b/android/src/org/purplei2p/i2pd/DaemonSingleton.java deleted file mode 100644 index e9e4fc06..00000000 --- a/android/src/org/purplei2p/i2pd/DaemonSingleton.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.purplei2p.i2pd; - -import java.util.HashSet; -import java.util.Set; -import android.os.Environment; -import android.util.Log; - -import org.purplei2p.i2pd.R; - -public class DaemonSingleton { - private static final String TAG = "i2pd"; - private static final DaemonSingleton instance = new DaemonSingleton(); - - public interface StateUpdateListener { - void daemonStateUpdate(); - } - - private final Set 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/android/src/org/purplei2p/i2pd/DaemonWrapper.java b/android/src/org/purplei2p/i2pd/DaemonWrapper.java new file mode 100644 index 00000000..7f9fcd93 --- /dev/null +++ b/android/src/org/purplei2p/i2pd/DaemonWrapper.java @@ -0,0 +1,387 @@ +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.Context; +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(); + } + + 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(); + } + + 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; + } + + 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); + 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); + } + } + + 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); + } + } + } + + /** + * 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 = assetManager; + + // 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 = 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() + "'"); + } + + public 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/android/src/org/purplei2p/i2pd/ForegroundService.java b/android/src/org/purplei2p/i2pd/ForegroundService.java index c1b1cc26..c1c918ac 100644 --- a/android/src/org/purplei2p/i2pd/ForegroundService.java +++ b/android/src/org/purplei2p/i2pd/ForegroundService.java @@ -19,8 +19,12 @@ public class ForegroundService extends Service { private volatile boolean shown; - private final DaemonSingleton.StateUpdateListener daemonStateUpdatedListener = - new DaemonSingleton.StateUpdateListener() { + private static ForegroundService instance; + + private static volatile DaemonWrapper daemon; + + private final DaemonWrapper.StateUpdateListener daemonStateUpdatedListener = + new DaemonWrapper.StateUpdateListener() { @Override public void daemonStateUpdate() { @@ -40,7 +44,7 @@ public class ForegroundService extends Service { // 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 @@ -53,16 +57,25 @@ public class ForegroundService extends Service { } } + public static void init(DaemonWrapper daemon) { + ForegroundService.daemon = daemon; + initCheck(); + } + + private static void initCheck() { + if(instance!=null && daemon!=null) instance.setListener(); + } + @Override public void onCreate() { 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); + if (!shown) daemonStateUpdatedListener.daemonStateUpdate(); } @Override @@ -73,8 +86,17 @@ 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() { + if(daemon!=null && instance!=null)daemon.removeStateChangeListener(instance.daemonStateUpdatedListener); } private synchronized void cancelNotification() { @@ -101,35 +123,39 @@ 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()); + 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); + // 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() : ""; + // 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(); + // 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; + // Send the notification. + //mNM.notify(NOTIFICATION, notification); + startForeground(NOTIFICATION, notification); + shown = true; + } } @RequiresApi(Build.VERSION_CODES.O) @@ -144,6 +170,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/android/src/org/purplei2p/i2pd/I2PDActivity.java b/android/src/org/purplei2p/i2pd/I2PDActivity.java index 777ca748..0801a655 100755 --- a/android/src/org/purplei2p/i2pd/I2PDActivity.java +++ b/android/src/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; @@ -24,7 +16,6 @@ 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; @@ -33,7 +24,6 @@ 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; @@ -60,25 +50,19 @@ 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 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) @@ -88,9 +72,9 @@ public class I2PDActivity extends Activity { 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)) : ""; + 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); @@ -117,6 +101,12 @@ 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); @@ -145,15 +135,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 { @@ -289,13 +277,13 @@ public class I2PDActivity extends Activity { case R.id.action_start_webview: setContentView(R.layout.webview); - this.webView = (WebView) findViewById(R.id.webview1); - this.webView.setWebViewClient(new WebViewClient()); + final WebView webView = findViewById(R.id.webview1); + webView.setWebViewClient(new WebViewClient()); - WebSettings webSettings = this.webView.getSettings(); + final WebSettings webSettings = webView.getSettings(); webSettings.setBuiltInZoomControls(true); webSettings.setJavaScriptEnabled(false); - this.webView.loadUrl("http://127.0.0.1:7070"); // TODO: instead 7070 I2Pd....HttpPort + webView.loadUrl("http://127.0.0.1:7070"); // TODO: instead 7070 I2Pd....HttpPort break; } @@ -335,7 +323,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; } @@ -427,167 +415,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); @@ -642,33 +469,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) { From db3e48a81a87c8ce70e727f815c011c7a3f62d52 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 24 Oct 2020 03:52:53 +0800 Subject: [PATCH 2/3] android: more logical daemon state changes --- .../src/org/purplei2p/i2pd/DaemonWrapper.java | 41 +++---- .../org/purplei2p/i2pd/ForegroundService.java | 111 ++++++++++-------- .../src/org/purplei2p/i2pd/I2PDActivity.java | 53 +++++---- 3 files changed, 104 insertions(+), 101 deletions(-) diff --git a/android/src/org/purplei2p/i2pd/DaemonWrapper.java b/android/src/org/purplei2p/i2pd/DaemonWrapper.java index 7f9fcd93..414a3ed1 100644 --- a/android/src/org/purplei2p/i2pd/DaemonWrapper.java +++ b/android/src/org/purplei2p/i2pd/DaemonWrapper.java @@ -12,7 +12,6 @@ import java.util.HashSet; import java.util.Set; import android.annotation.TargetApi; -import android.content.Context; import android.content.res.AssetManager; import android.net.ConnectivityManager; import android.net.Network; @@ -32,7 +31,7 @@ public class DaemonWrapper { private boolean assetsCopied; public interface StateUpdateListener { - void daemonStateUpdate(); + void daemonStateUpdate(State oldValue, State newValue); } private final Set stateUpdateListeners = new HashSet<>(); @@ -58,7 +57,7 @@ public class DaemonWrapper { return; state = newState; - fireStateUpdate1(); + fireStateUpdate1(oldState, newState); } public synchronized void stopAcceptingTunnels() { @@ -81,12 +80,10 @@ public class DaemonWrapper { } } - public synchronized int GetTransitTunnelsCount() { + public int getTransitTunnelsCount() { return I2PD_JNI.GetTransitTunnelsCount(); } - private volatile boolean startedOkay; - public enum State { uninitialized(R.string.uninitialized), starting(R.string.starting), @@ -105,7 +102,11 @@ public class DaemonWrapper { public int getStatusStringResourceId() { return statusStringResourceId; } - }; + + public boolean isStartedOkay() { + return equals(State.startedOkay) || equals(State.gracefulShutdownInProgress); + } + } private volatile State state = State.uninitialized; @@ -134,7 +135,6 @@ public class DaemonWrapper { daemonStartResult = I2PD_JNI.startDaemon(); if ("ok".equals(daemonStartResult)) { setState(State.startedOkay); - setStartedOkay(true); } else setState(State.startFailed); } @@ -148,11 +148,11 @@ public class DaemonWrapper { private Throwable lastThrowable; private String daemonStartResult = "N/A"; - private void fireStateUpdate1() { + private void fireStateUpdate1(State oldValue, State newValue) { Log.i(TAG, "daemon state change: " + state); for (StateUpdateListener listener : stateUpdateListeners) { try { - listener.daemonStateUpdate(); + listener.daemonStateUpdate(oldValue, newValue); } catch (Throwable tr) { Log.e(TAG, "exception in listener ignored", tr); } @@ -167,18 +167,8 @@ public class DaemonWrapper { 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; - } + return getState().isStartedOkay(); } public synchronized void stopDaemon() { @@ -189,7 +179,6 @@ public class DaemonWrapper { Log.e(TAG, "", tr); } - setStartedOkay(false); setState(State.stopped); } } @@ -197,7 +186,7 @@ public class DaemonWrapper { private void processAssets() { if (!assetsCopied) { try { - assetsCopied = true; // prevent from running on every state update + assetsCopied = true; File holderFile = new File(i2pdpath, "assets.ready"); String versionName = BuildConfig.VERSION_NAME; // here will be app version, like 2.XX.XX @@ -283,12 +272,10 @@ public class DaemonWrapper { * Path to asset, relative to app's assets directory. */ private void copyAsset(String path) { - AssetManager manager = assetManager; - // If we have a directory, we make it and recurse. If a file, we copy its // contents. try { - String[] contents = manager.list(path); + 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 @@ -355,7 +342,7 @@ public class DaemonWrapper { Log.e(TAG, "fileOrDirectory.delete() returned " + deleteResult + ", absolute path='" + fileOrDirectory.getAbsolutePath() + "'"); } - public void registerNetworkCallback(){ + private void registerNetworkCallback(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) registerNetworkCallback0(); } diff --git a/android/src/org/purplei2p/i2pd/ForegroundService.java b/android/src/org/purplei2p/i2pd/ForegroundService.java index c1c918ac..c97b7f1f 100644 --- a/android/src/org/purplei2p/i2pd/ForegroundService.java +++ b/android/src/org/purplei2p/i2pd/ForegroundService.java @@ -23,22 +23,28 @@ public class ForegroundService extends Service { private static volatile DaemonWrapper daemon; + private static final Object initDeinitLock = new Object(); + 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; @@ -63,7 +69,9 @@ public class ForegroundService extends Service { } private static void initCheck() { - if(instance!=null && daemon!=null) instance.setListener(); + synchronized (initDeinitLock) { + if (instance != null && daemon != null) instance.setListener(); + } } @Override @@ -75,7 +83,7 @@ public class ForegroundService extends Service { private void setListener() { daemon.addStateChangeListener(daemonStateUpdatedListener); - if (!shown) daemonStateUpdatedListener.daemonStateUpdate(); + updateNotificationText(); } @Override @@ -96,18 +104,23 @@ public class ForegroundService extends Service { } private static void deinitCheck() { - if(daemon!=null && instance!=null)daemon.removeStateChangeListener(instance.daemonStateUpdatedListener); + 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 @@ -122,39 +135,41 @@ public class ForegroundService extends Service { /** * Show a notification while this service is running. */ - private synchronized void showNotification() { - 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()); + 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); + // 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() : ""; + // 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(); + // 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; + // Send the notification. + //mNM.notify(NOTIFICATION, notification); + startForeground(NOTIFICATION, notification); + shown = true; + } } } diff --git a/android/src/org/purplei2p/i2pd/I2PDActivity.java b/android/src/org/purplei2p/i2pd/I2PDActivity.java index 0801a655..97870e63 100755 --- a/android/src/org/purplei2p/i2pd/I2PDActivity.java +++ b/android/src/org/purplei2p/i2pd/I2PDActivity.java @@ -7,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; @@ -18,9 +17,6 @@ import android.content.ServiceConnection; import android.content.SharedPreferences; 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; @@ -36,7 +32,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; @@ -62,26 +57,31 @@ public class I2PDActivity extends Activity { private final DaemonWrapper.StateUpdateListener daemonStateUpdatedListener = new DaemonWrapper.StateUpdateListener() { @Override - public void daemonStateUpdate() { - 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); - } - }); + 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; @@ -110,7 +110,7 @@ public class I2PDActivity extends Activity { 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) { @@ -372,9 +372,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); @@ -390,7 +391,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*/); From bfcf3cfbf132ff904bcfca6557da061db4090bd5 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 24 Oct 2020 12:40:22 +0800 Subject: [PATCH 3/3] Fixes #1563 --- android/AndroidManifest.xml | 11 ++--- android/build.gradle | 4 +- .../{webview.xml => activity_web_console.xml} | 4 +- .../src/org/purplei2p/i2pd/I2PDActivity.java | 11 +---- .../purplei2p/i2pd/WebConsoleActivity.java | 41 +++++++++++++++++++ 5 files changed, 55 insertions(+), 16 deletions(-) rename android/res/layout/{webview.xml => activity_web_console.xml} (82%) create mode 100644 android/src/org/purplei2p/i2pd/WebConsoleActivity.java diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 88985138..8b011b8e 100755 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -15,10 +15,10 @@ 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"> + @@ -31,10 +31,10 @@ android:label="@string/app_name"> + - @@ -52,4 +52,5 @@ android:value="org.purplei2p.i2pd.I2PDPermsAskerActivity" /> - + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index ace2047a..7d25e28b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,6 +21,8 @@ repositories { dependencies { implementation 'androidx.core:core:1.0.2' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' } android { @@ -90,7 +92,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/android/res/layout/webview.xml b/android/res/layout/activity_web_console.xml similarity index 82% rename from android/res/layout/webview.xml rename to android/res/layout/activity_web_console.xml index 887896a7..5724b387 100644 --- a/android/res/layout/webview.xml +++ b/android/res/layout/activity_web_console.xml @@ -1,12 +1,14 @@ + + tools:context=".WebConsoleActivity"> + diff --git a/android/src/org/purplei2p/i2pd/I2PDActivity.java b/android/src/org/purplei2p/i2pd/I2PDActivity.java index 97870e63..9645d4f7 100755 --- a/android/src/org/purplei2p/i2pd/I2PDActivity.java +++ b/android/src/org/purplei2p/i2pd/I2PDActivity.java @@ -276,15 +276,8 @@ public class I2PDActivity extends Activity { return true; case R.id.action_start_webview: - setContentView(R.layout.webview); - final 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 - break; + startActivity(new Intent(getApplicationContext(), WebConsoleActivity.class)); + return true; } return super.onOptionsItemSelected(item); diff --git a/android/src/org/purplei2p/i2pd/WebConsoleActivity.java b/android/src/org/purplei2p/i2pd/WebConsoleActivity.java new file mode 100644 index 00000000..24e6baeb --- /dev/null +++ b/android/src/org/purplei2p/i2pd/WebConsoleActivity.java @@ -0,0 +1,41 @@ +package org.purplei2p.i2pd; + +import androidx.appcompat.app.AppCompatActivity; + +import android.app.Activity; +import android.os.Bundle; +import android.view.MenuItem; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import java.util.Objects; + +public class WebConsoleActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_web_console); + + Objects.requireNonNull(getActionBar()).setDisplayHomeAsUpEnabled(true); + + final 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 + } + + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id==android.R.id.home) { + finish(); + return true; + } + return false; + } +}