From c875709a814198dfbb2a4762feec44f989282bf8 Mon Sep 17 00:00:00 2001 From: nonlin-lin-chaos-order-etc-etal Date: Thu, 4 Apr 2024 14:00:21 +0800 Subject: [PATCH] Fix #50 --- .gitignore | 1 + README.md | 8 +- app/build.gradle | 18 +- .../org/purplei2p/i2pd/DaemonWrapper.java | 76 +- .../java/org/purplei2p/i2pd/I2PDActivity.java | 41 +- .../java/org/purplei2p/i2pd/I2PD_JNI.java | 35 - .../main/java/org/purplei2p/i2pd/I2pdApi.java | 99 ++ .../i2pd/NetworkStateChangeReceiver.java | 2 +- .../org/purplei2p/i2pd/SettingsActivity.java | 8 +- .../purplei2p/i2pd/WebConsoleActivity.java | 2 +- .../purplei2p/i2pd/iniedotr/IniEditor.java | 1182 ----------------- app/src/main/res/layout/activity_main.xml | 4 +- app/src/main/res/values/strings.xml | 9 +- binary/jni/README.md | 20 + binary/jni/build_all.sh | 9 + binary/jni/ndkbuild-wrapper.sh | 10 + gradle/wrapper/gradle-wrapper.properties | 6 +- 17 files changed, 214 insertions(+), 1316 deletions(-) delete mode 100644 app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java create mode 100644 app/src/main/java/org/purplei2p/i2pd/I2pdApi.java delete mode 100644 app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java create mode 100644 binary/jni/README.md create mode 100755 binary/jni/build_all.sh create mode 100755 binary/jni/ndkbuild-wrapper.sh diff --git a/.gitignore b/.gitignore index 31c4496..842b789 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ build *.local *.jks .vscode +**/tmp* diff --git a/README.md b/README.md index f265d6e..fecc076 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This repository contains Android application sources of i2pd sudo apt-get install g++ openjdk-11-jdk gradle ``` -If your system provides gradle with version < 5.1, download it from gradle homepage: +If your system provides gradle with version < 5.1, download it from gradle homepage: https://gradle.org/install/ @@ -54,10 +54,8 @@ export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export ANDROID_HOME=/opt/android-sdk export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/23.2.8568313 -pushd app/jni -./build_boost.sh -./build_openssl.sh -./build_miniupnpc.sh +pushd binary/jni +./build_all.sh popd gradle clean assembleDebug diff --git a/app/build.gradle b/app/build.gradle index 5c94bfc..b29cb6c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,8 +28,15 @@ android { externalNativeBuild { ndkBuild { - arguments "NDK_MODULE_PATH:=${rootProject.projectDir}/app/jni" + arguments "NDK_MODULE_PATH:=${rootProject.projectDir}/binary/jni" arguments "-j${Runtime.getRuntime().availableProcessors()}" + arguments 'V=1' + //targets + // You need to configure this executable and its sources in your + // Android.mk file like you would any other library, except you must + // specify "include $(BUILD_EXECUTABLE)". Building executables from + // your native sources is optional, and building native libraries to + // package into your APK satisfies most project requirements. } } } @@ -59,16 +66,19 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' } debug { - jniDebuggable = true } } externalNativeBuild { ndkBuild { - path "${rootProject.projectDir}/app/jni/Android.mk" + path "${rootProject.projectDir}/binary/jni/Android.mk" + } + } + sourceSets { + main { + jniLibs.srcDir file("${rootProject.projectDir}/binary/libs") } } - compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java index 5ad2ac0..531ff79 100644 --- a/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java +++ b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java @@ -13,6 +13,7 @@ import java.util.Set; import java.util.Locale; import android.annotation.TargetApi; +import android.content.Context; import android.content.res.AssetManager; import android.net.ConnectivityManager; import android.net.Network; @@ -32,7 +33,9 @@ public class DaemonWrapper { private String i2pdpath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd"; private boolean assetsCopied; - private static final String appLocale = Locale.getDefault().getDisplayLanguage(Locale.ENGLISH).toLowerCase(); // lower-case system language (like "english") + private String getAppLocale() { + return Locale.getDefault().getDisplayLanguage(Locale.ENGLISH).toLowerCase(); // lower-case system language (like "english") + } public interface StateUpdateListener { void daemonStateUpdate(State oldValue, State newValue); @@ -67,25 +70,25 @@ public class DaemonWrapper { public synchronized void stopAcceptingTunnels() { if (isStartedOkay()) { setState(State.gracefulShutdownInProgress); - I2PD_JNI.stopAcceptingTunnels(); + I2pdApi.stopAcceptingTunnels(); } } public synchronized void startAcceptingTunnels() { if (isStartedOkay()) { setState(State.startedOkay); - I2PD_JNI.startAcceptingTunnels(); + I2pdApi.startAcceptingTunnels(); } } public synchronized void reloadTunnelsConfigs() { if (isStartedOkay()) { - I2PD_JNI.reloadTunnelsConfigs(); + I2pdApi.reloadTunnelsConfigs(); } } public int getTransitTunnelsCount() { - return I2PD_JNI.getTransitTunnelsCount(); + return I2pdApi.getTransitTunnelsCount(); } public enum State { @@ -117,11 +120,11 @@ public class DaemonWrapper { return state; } - public DaemonWrapper(AssetManager assetManager, ConnectivityManager connectivityManager){ + public DaemonWrapper(Context ctx, AssetManager assetManager, ConnectivityManager connectivityManager){ this.assetManager = assetManager; this.connectivityManager = connectivityManager; setState(State.starting); - startDaemon(); + startDaemon(ctx); } private Throwable lastThrowable; @@ -146,16 +149,6 @@ public class DaemonWrapper { return daemonStartResult; } - public static String getDataDir() { // for settings iniEditor - return I2PD_JNI.getDataDir(); - } - - public void changeDataDir(String dataDir, Boolean updateAssets) { - I2PD_JNI.setDataDir(dataDir); - if (updateAssets) processAssets(); - //ToDo: move old dir to new dir? - } - public boolean isStartedOkay() { return getState().isStartedOkay(); } @@ -163,19 +156,18 @@ public class DaemonWrapper { public synchronized void stopDaemon() { if (isStartedOkay()) { try { - I2PD_JNI.stopDaemon(); + I2pdApi.stopDaemon(); } catch (Throwable tr) { Log.e(TAG, "", tr); } setState(State.stopped); } } - public synchronized void startDaemon() { + public synchronized void startDaemon(Context ctx) { if( getState() != State.stopped && getState() != State.starting ) return; new Thread(() -> { try { processAssets(); - I2PD_JNI.loadLibraries(); //registerNetworkCallback(); } catch (Throwable tr) { lastThrowable = tr; @@ -184,12 +176,10 @@ public class DaemonWrapper { } try { synchronized (DaemonWrapper.this) { - I2PD_JNI.setDataDir(i2pdpath); // (Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd"); - - Log.i(TAG, "setting webconsole language to " + appLocale); - I2PD_JNI.setLanguage(appLocale); + String locale = getAppLocale(); + Log.i(TAG, "setting webconsole language to " + locale); - daemonStartResult = I2PD_JNI.startDaemon(); + daemonStartResult = I2pdApi.startDaemon(ctx, i2pdpath, locale); if ("ok".equals(daemonStartResult)) { setState(State.startedOkay); } else @@ -209,31 +199,13 @@ public class DaemonWrapper { Log.d(TAG, "checking assets"); if (holderFile.exists()) { - try { // if holder file exists, read assets version string - FileReader fileReader = new FileReader(holderFile); - - try { - BufferedReader br = new BufferedReader(fileReader); - - try { + try (FileReader fileReader = new FileReader(holderFile)) { // if holder file exists, read assets version string + try (BufferedReader br = new BufferedReader(fileReader)) { 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); @@ -258,15 +230,9 @@ public class DaemonWrapper { copyAsset("tunnels.conf"); // update holder file about successful copying - FileWriter writer = new FileWriter(holderFile); - try { + ; + try (FileWriter writer = new FileWriter(holderFile)) { writer.append(versionName); - } finally { - try { - writer.close(); - } catch (IOException e) { - Log.e(TAG,"on writer close", e); - } } } catch (Throwable tr) @@ -372,14 +338,14 @@ public class DaemonWrapper { @Override public void onAvailable(Network network) { super.onAvailable(network); - I2PD_JNI.onNetworkStateChanged(true); + I2pdApi.onNetworkStateChanged(true); Log.d(TAG, "NetworkCallback.onAvailable"); } @Override public void onLost(Network network) { super.onLost(network); - I2PD_JNI.onNetworkStateChanged(false); + I2pdApi.onNetworkStateChanged(false); Log.d(TAG, " NetworkCallback.onLost"); } } diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java index 0c0ecc7..febfe4e 100644 --- a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java +++ b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java @@ -45,11 +45,11 @@ public class I2PDActivity extends Activity { public static final String PACKAGE_URI_SCHEME = "package:"; private TextView textView; - private CheckBox HTTPProxyState; - private CheckBox SOCKSProxyState; - private CheckBox BOBState; - private CheckBox SAMState; - private CheckBox I2CPState; +// private CheckBox HTTPProxyState; +// private CheckBox SOCKSProxyState; +// private CheckBox BOBState; +// private CheckBox SAMState; +// private CheckBox I2CPState; private static volatile DaemonWrapper daemon; @@ -75,13 +75,13 @@ public class I2PDActivity extends Activity { DaemonWrapper.State state = daemon.getState(); - if (daemon.isStartedOkay()) { - HTTPProxyState.setChecked(I2PD_JNI.getHTTPProxyState()); - SOCKSProxyState.setChecked(I2PD_JNI.getSOCKSProxyState()); - BOBState.setChecked(I2PD_JNI.getBOBState()); - SAMState.setChecked(I2PD_JNI.getSAMState()); - I2CPState.setChecked(I2PD_JNI.getI2CPState()); - } +// if (daemon.isStartedOkay()) { +// HTTPProxyState.setChecked(I2pdApi.getHTTPProxyState()); +// SOCKSProxyState.setChecked(I2pdApi.getSOCKSProxyState()); +// BOBState.setChecked(I2pdApi.getBOBState()); +// SAMState.setChecked(I2pdApi.getSAMState()); +// I2CPState.setChecked(I2pdApi.getI2CPState()); +// } 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)) : ""; @@ -113,15 +113,15 @@ public class I2PDActivity extends Activity { setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.appStatusText); - HTTPProxyState = (CheckBox) findViewById(R.id.service_httpproxy_box); - SOCKSProxyState = (CheckBox) findViewById(R.id.service_socksproxy_box); - BOBState = (CheckBox) findViewById(R.id.service_bob_box); - SAMState = (CheckBox) findViewById(R.id.service_sam_box); - I2CPState = (CheckBox) findViewById(R.id.service_i2cp_box); +// HTTPProxyState = (CheckBox) findViewById(R.id.service_httpproxy_box); +// SOCKSProxyState = (CheckBox) findViewById(R.id.service_socksproxy_box); +// BOBState = (CheckBox) findViewById(R.id.service_bob_box); +// SAMState = (CheckBox) findViewById(R.id.service_sam_box); +// I2CPState = (CheckBox) findViewById(R.id.service_i2cp_box); if (daemon == null) { ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - daemon = new DaemonWrapper(getAssets(), connectivityManager); + daemon = new DaemonWrapper(getApplicationContext(), getAssets(), connectivityManager); } ForegroundService.init(daemon); @@ -261,6 +261,8 @@ public class I2PDActivity extends Activity { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.options_main, menu); menu.findItem(R.id.action_battery_otimizations).setVisible(isBatteryOptimizationsOpenOsDialogApiAvailable()); + //TODO + menu.findItem(R.id.action_reload_tunnels_config).setVisible(false); this.optionsMenu = menu; return true; } @@ -295,6 +297,7 @@ public class I2PDActivity extends Activity { return true; case R.id.action_reload_tunnels_config: + //TODO onReloadTunnelsConfig(); return true; @@ -302,7 +305,7 @@ public class I2PDActivity extends Activity { if(daemon.isStartedOkay()) startActivity(new Intent(getApplicationContext(), WebConsoleActivity.class)); else - Toast.makeText(this,"I2Pd not was started!", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.error_i2pd_not_running, Toast.LENGTH_SHORT).show(); return true; case R.id.action_settings: startActivity(new Intent(getApplicationContext(), SettingsActivity.class)); diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java b/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java deleted file mode 100644 index c1f7d1c..0000000 --- a/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.purplei2p.i2pd; - -public class I2PD_JNI { - public static native String getABICompiledWith(); - - public static void loadLibraries() { - System.loadLibrary("i2pd"); - } - - /** - * returns error info if failed - * returns "ok" if daemon initialized and started okay - */ - public static native String startDaemon(); - public static native void stopDaemon(); - - public static native void startAcceptingTunnels(); - public static native void stopAcceptingTunnels(); - public static native void reloadTunnelsConfigs(); - - public static native void setDataDir(String jdataDir); - public static native void setLanguage(String jlanguage); - - public static native int getTransitTunnelsCount(); - public static native String getWebConsAddr(); - public static native String getDataDir(); - - public static native boolean getHTTPProxyState(); - public static native boolean getSOCKSProxyState(); - public static native boolean getBOBState(); - public static native boolean getSAMState(); - public static native boolean getI2CPState(); - - public static native void onNetworkStateChanged(boolean isConnected); -} diff --git a/app/src/main/java/org/purplei2p/i2pd/I2pdApi.java b/app/src/main/java/org/purplei2p/i2pd/I2pdApi.java new file mode 100644 index 0000000..cb62d0f --- /dev/null +++ b/app/src/main/java/org/purplei2p/i2pd/I2pdApi.java @@ -0,0 +1,99 @@ +package org.purplei2p.i2pd; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** i2pd process API calls via TCP between the Android Java app and i2pd C++-only process. + * TODO + */ +public class I2pdApi { + private static String dataDir; + private static Process i2pdProcess; + private static final String TAG = "I2pdApi"; + + /** + * returns error info if failed + * returns "ok" if daemon initialized and started okay + */ + public static String startDaemon(Context ctx, String dataDir, String language){ + try { + I2pdApi.dataDir = dataDir; + Process p = I2pdApi.i2pdProcess = Runtime.getRuntime().exec(new String[] { + ctx.getApplicationInfo().nativeLibraryDir+"/libi2pd.so", + "--datadir="+dataDir + }); + new Thread(() -> { + try { + try (BufferedInputStream bis = new BufferedInputStream(p.getInputStream())) { + try (InputStreamReader sr = new InputStreamReader(bis)) { + try (BufferedReader r = new BufferedReader(sr)) { + while(true){ + String s = r.readLine(); + if (s == null) break; + Log.i(TAG, s); + } + } + } + } + }catch(Throwable tr){ + Log.e(TAG, "", tr); + } + }, "i2pd-stdout"); + new Thread(() -> { + try { + try (BufferedInputStream bis = new BufferedInputStream(p.getErrorStream())) { + try (InputStreamReader sr = new InputStreamReader(bis)) { + try (BufferedReader r = new BufferedReader(sr)) { + while(true){ + String s = r.readLine(); + if (s == null) break; + Log.i(TAG, s); + } + } + } + } + }catch(Throwable tr){ + Log.e(TAG, "", tr); + } + }, "i2pd-stderr"); + return "ok"; + } catch (Throwable tr) { + Log.e(TAG, "", tr); + return "Error in exec(): " + tr; + } + } + + public static void stopDaemon(){ + Process p = i2pdProcess; + if (p != null) { + try { + p.destroy(); + } catch (Throwable tr) { + Log.e(TAG, "", tr); + } + i2pdProcess = null; + } + } + + public static void startAcceptingTunnels(){} + public static void stopAcceptingTunnels(){} + public static void reloadTunnelsConfigs(){} + + public static int getTransitTunnelsCount(){return -1;} + public static String getWebConsAddr(){return "";} + public static String getDataDir() { + return dataDir; + } + + public static boolean getHTTPProxyState(){return false;} + public static boolean getSOCKSProxyState(){return false;} + public static boolean getBOBState(){return false;} + public static boolean getSAMState(){return false;} + public static boolean getI2CPState(){return false;} + + public static void onNetworkStateChanged(boolean isConnected){} +} diff --git a/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java b/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java index 35975a0..a440d99 100644 --- a/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java +++ b/app/src/main/java/org/purplei2p/i2pd/NetworkStateChangeReceiver.java @@ -19,7 +19,7 @@ public class NetworkStateChangeReceiver extends BroadcastReceiver { NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo(); boolean isConnected = activeNetworkInfo != null && activeNetworkInfo.isConnected(); - I2PD_JNI.onNetworkStateChanged(isConnected); + I2pdApi.onNetworkStateChanged(isConnected); } catch (Throwable tr) { Log.e(TAG, "", tr); } diff --git a/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java b/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java index 43d9b1a..0075959 100644 --- a/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java +++ b/app/src/main/java/org/purplei2p/i2pd/SettingsActivity.java @@ -18,11 +18,9 @@ import java.util.List; import java.util.Objects; -//import org.purplei2p.i2pd.iniedotr.IniEditor; public class SettingsActivity extends Activity { - //protected IniEditor iniedit = new IniEditor(); - private String TAG = "i2pdSrvcSettings"; + private static final String TAG = "i2pdSttngs"; private File cacheDir; public static String onBootFileName = "/onBoot"; // just file, empty, if exist the do autostart, if not then no. @@ -59,7 +57,7 @@ public class SettingsActivity extends Activity { startActivity(intent); } } catch (Exception e) { - Log.e("exceptionAutostarti2pd" , String.valueOf(e)); + Log.e("Autostarti2pd" , "", e); } } @@ -99,7 +97,7 @@ public class SettingsActivity extends Activity { if (!onBoot.createNewFile()) Log.d(TAG, "Cant create new wile on: "+onBoot.getAbsolutePath()); } catch (Exception e) { - Log.e(TAG, "error: " + e.toString()); + Log.e(TAG, "", e); } } } else { diff --git a/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java b/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java index e96d068..9f563fb 100644 --- a/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java +++ b/app/src/main/java/org/purplei2p/i2pd/WebConsoleActivity.java @@ -27,7 +27,7 @@ public class WebConsoleActivity extends Activity { final WebSettings webSettings = webView.getSettings(); webSettings.setBuiltInZoomControls(true); webSettings.setJavaScriptEnabled(false); - webView.loadUrl(I2PD_JNI.getWebConsAddr()); + webView.loadUrl(I2pdApi.getWebConsAddr()); swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe); swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { diff --git a/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java b/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java deleted file mode 100644 index 8bb2b4c..0000000 --- a/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java +++ /dev/null @@ -1,1182 +0,0 @@ -/* - IniEditor is Copyright (c) 2003-2013, Nik Haldimann - All rights reserved. Distributed under a BSD-style license. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS - IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -package org.purplei2p.i2pd.iniedotr; - -import java.io.*; -import java.util.*; - -/** - * Loads, edits and saves INI-style configuration files. While loading from and - * saving to streams and files, IniEditor preserves comments and - * blank lines as well as the order of sections and lines in general. - *

- * IniEditor assumes configuration files to be split in sections. - * A section starts out with a header, which consists of the section name - * enclosed in brackets ('[' and ']'). Everything - * before the first section header is ignored when loading from a stream or - * file. The {@link IniEditor.Section} class can be used to load - * configuration files without sections (ie Java-style properties). - *

- * A "common section" may be named. All sections inherit the options of this - * section but can overwrite them. - *

- * IniEditor represents an INI file (or rather, its sections) line - * by line, as comment, blank and option lines. A comment is a line which has a - * comment delimiter as its first non-white space character. The default comment - * delimiters, which may be overwritten, are '#' and - * ';'. - *

- * A blank line is any line that consists only of white space. - *

- * Everything else is an option line. Option names and values are separated by - * option delimiters '=', ':' or white space (spaces - * and tabs). - *

- * Here's a minimal example. Suppose, we have this in a file called - * users.ini: - *

- *   [root]
- *   role = administrator
- *   last_login = 2003-05-04
- *
- *   [joe]
- *   role = author
- *   last_login = 2003-05-13
- * 
- * Let's load that file, add something to it and save the changes: - *
- *   IniEditor users = new IniEditor();
- *   users.load("users.ini");
- *   users.set("root", "last_login", "2003-05-16");
- *   users.addComment("root", "Must change password often");
- *   users.set("root", "change_pwd", "10 days");
- *   users.addBlankLine("root");
- *   users.save("users.ini");
- * 
- * Now, the file looks like this: - *
- *   [root]
- *   role = administrator
- *   last_login = 2003-05-16
- *
- *   # Must change password often
- *   change_pwd = 10 days
- *
- *   [joe]
- *   role = author
- *   last_login = 2003-05-13
- * 
- *

- * IniEditor provides services simliar to the standard Java API class - * java.util.Properties. It uses its own parser, though, which - * differs in these respects from that of Properties: - *

- * - * @author Nik Haldimann, nhaldimann at gmail dot com - * @version r5 (3/4/2013) - */ -public class IniEditor { - - private static boolean DEFAULT_CASE_SENSITIVITY = false; - - private Map sections; - private List sectionOrder; - private String commonName; - private char[] commentDelims; - private boolean isCaseSensitive; - private OptionFormat optionFormat; - - /** - * Constructs new bare IniEditor instance. - */ - public IniEditor() { - this(null, null); - } - - /** - * Constructs new bare IniEditor instance specifying case-sensitivity. - * - * @param isCaseSensitive section and option names are case-sensitive if - * this is true - */ - public IniEditor(boolean isCaseSensitive) { - this(null, null, isCaseSensitive); - } - - /** - * Constructs new IniEditor instance with a common section. Options in the - * common section are used as defaults for all other sections. - * - * @param commonName name of the common section - */ - public IniEditor(String commonName) { - this(commonName, null); - } - - /** - * Constructs new IniEditor instance with a common section. Options in the - * common section are used as defaults for all other sections. - * - * @param commonName name of the common section - * @param isCaseSensitive section and option names are case-sensitive if - * this is true - */ - public IniEditor(String commonName, boolean isCaseSensitive) { - this(commonName, null, isCaseSensitive); - } - - /** - * Constructs new IniEditor defining comment delimiters. - * - * @param delims an array of characters to be recognized as starters of - * comment lines; the first of them will be used for newly created - * comments - */ - public IniEditor(char[] delims) { - this(null, delims); - } - - /** - * Constructs new IniEditor defining comment delimiters. - * - * @param delims an array of characters to be recognized as starters of - * comment lines; the first of them will be used for newly created - * comments - * @param isCaseSensitive section and option names are case-sensitive if - * this is true - */ - public IniEditor(char[] delims, boolean isCaseSensitive) { - this(null, delims, isCaseSensitive); - } - - /** - * Constructs new IniEditor instance with a common section, defining - * comment delimiters. Options in the common section are used as defaults - * for all other sections. - * - * @param commonName name of the common section - * @param delims an array of characters to be recognized as starters of - * comment lines; the first of them will be used for newly created - * comments - */ - public IniEditor(String commonName, char[] delims) { - this(commonName, delims, DEFAULT_CASE_SENSITIVITY); - } - - /** - * Constructs new IniEditor instance with a common section, defining - * comment delimiters. Options in the common section are used as defaults - * for all other sections. - * - * @param commonName name of the common section - * @param delims an array of characters to be recognized as starters of - * comment lines; the first of them will be used for newly created - * comments - */ - public IniEditor(String commonName, char[] delims, boolean isCaseSensitive) { - this.sections = new HashMap(); - this.sectionOrder = new LinkedList(); - this.isCaseSensitive = isCaseSensitive; - if (commonName != null) { - this.commonName = commonName; - addSection(this.commonName); - } - this.commentDelims = delims; - this.optionFormat = new OptionFormat(Section.DEFAULT_OPTION_FORMAT); - } - - /** - * Sets the option format for this instance to the given string. Options - * will be rendered according to the given format string when printed. The - * string must contain %s three times, these will be replaced - * with the option name, the option separator and the option value in this - * order. Literal percentage signs must be escaped by preceding them with - * another percentage sign (i.e., %% corresponds to one - * percentage sign). The default format string is "%s %s %s". - * - * Option formats may look like format strings as supported by Java 1.5, but - * the string is in fact parsed in a custom fashion to guarantee backwards - * compatibility. So don't try clever stuff like using format conversion - * types other than %s. - * - * @param formatString a format string, containing %s exactly - * three times - * @throws IllegalArgumentException if the format string is illegal - */ - public void setOptionFormatString(String formatString) { - this.optionFormat = new OptionFormat(formatString); - } - - /** - * Returns the value of a given option in a given section or null if either - * the section or the option don't exist. If a common section was defined - * options are also looked up there if they're not present in the specific - * section. - * - * @param section the section's name - * @param option the option's name - * @return the option's value - * @throws NullPointerException any of the arguments is null - */ - public String get(String section, String option) { - if (hasSection(section)) { - Section sect = getSection(section); - if (sect.hasOption(option)) { - return sect.get(option); - } - if (this.commonName != null) { - return getSection(this.commonName).get(option); - } - } - return null; - } - - /** - * Returns a map of a given section with option/value pairs or null if the - * section doesn't exist. - * - * @param section the section's name - * @return HashMap of option/value pairs from the section - * @throws NullPointerException if section is null - */ - public Map< String, String > getSectionMap(String section) { - Map< String, String > sectionMap = new HashMap< String, String >(); - if (hasSection(section)) { - Section sect = getSection(section); - for(String key : sect.options.keySet()) { - sectionMap.put(key, sect.options.get(key).value); - } - return sectionMap; - } - return null; - } - - /** - * Sets the value of an option in a section, if the option exist, otherwise - * adds the option to the section. Trims white space from the start and the - * end of the value and deletes newline characters it might contain. - * - * @param section the section's name - * @param option the option's name - * @param value the option's value - * @throws IniEditor.NoSuchSectionException no section with the given name exists - * @throws IllegalArgumentException the option name is illegal, - * ie contains a '=' character or consists only of white space - * @throws NullPointerException section or option are null - */ - public void set(String section, String option, String value) { - if (hasSection(section)) { - getSection(section).set(option, value); - } - else { - throw new NoSuchSectionException(section); - } - } - - /** - * Removes an option from a section if it exists. Will not remove options - * from the common section if it's not directly addressed. - * - * @param section the section's name - * @param option the option's name - * @return true if the option was actually removed - * @throws IniEditor.NoSuchSectionException no section with the given name exists - */ - public boolean remove(String section, String option) { - if (hasSection(section)) { - return getSection(section).remove(option); - } - else { - throw new NoSuchSectionException(section); - } - } - - /** - * Checks whether an option exists in a given section. Options in the - * common section are assumed to not exist in particular sections, - * unless they're overwritten. - * - * @param section the section's name - * @param option the option's name - * @return true if the given section has the option - */ - public boolean hasOption(String section, String option) { - return hasSection(section) && getSection(section).hasOption(option); - } - - /** - * Checks whether a section with a particular name exists in this instance. - * - * @param name the name of the section - * @return true if the section exists - */ - public boolean hasSection(String name) { - return this.sections.containsKey(normSection(name)); - } - - /** - * Adds a section if it doesn't exist yet. - * - * @param name the name of the section - * @return true if the section didn't already exist - * @throws IllegalArgumentException the name is illegal, ie contains one - * of the characters '[' and ']' or consists only of white space - */ - public boolean addSection(String name) { - String normName = normSection(name); - if (!hasSection(normName)) { - // Section constructor might throw IllegalArgumentException - Section section = new Section(normName, this.commentDelims, - this.isCaseSensitive); - section.setOptionFormat(this.optionFormat); - this.sections.put(normName, section); - this.sectionOrder.add(normName); - return true; - } - else { - return false; - } - } - - /** - * Removes a section if it exists. - * - * @param name the section's name - * @return true if the section actually existed - * @throws IllegalArgumentException when trying to remove the common section - */ - public boolean removeSection(String name) { - String normName = normSection(name); - if (this.commonName != null && this.commonName.equals(normName)) { - throw new IllegalArgumentException("Can't remove common section"); - } - if (hasSection(normName)) { - this.sections.remove(normName); - this.sectionOrder.remove(normName); - return true; - } - else { - return false; - } - } - - /** - * Returns all section names in this instance minus the common section if - * one was defined. - * - * @return list of the section names in original/insertion order - */ - public List sectionNames() { - List sectList = new ArrayList(this.sectionOrder); - if (this.commonName != null) { - sectList.remove(this.commonName); - } - return sectList; - } - - /** - * Returns all option names of a section, not including options from the - * common section. - * - * @param section the section's name - * @return list of option names - * @throws IniEditor.NoSuchSectionException no section with the given name exists - */ - public List optionNames(String section) { - if (hasSection(section)) { - return getSection(section).optionNames(); - } - else { - throw new NoSuchSectionException(section); - } - } - - /** - * Adds a comment line to the end of a section. A comment spanning - * several lines (ie with line breaks) will be split up, one comment - * line for each line. - * - * @param section the section's name - * @param comment the comment - * @throws IniEditor.NoSuchSectionException no section with the given name exists - */ - public void addComment(String section, String comment) { - if (hasSection(section)) { - getSection(section).addComment(comment); - } - else { - throw new NoSuchSectionException(section); - } - } - - /** - * Adds a blank line to the end of a section. - * - * @param section the section's name - * @throws IniEditor.NoSuchSectionException no section with the given name exists - */ - public void addBlankLine(String section) { - if (hasSection(section)) { - getSection(section).addBlankLine(); - } - else { - throw new NoSuchSectionException(section); - } - } - - /** - * Writes this instance in INI format to a file. - * - * @param filename the file to write to - * @throws IOException at an I/O problem - */ - public void save(String filename) throws IOException { - save(new File(filename)); - } - - /** - * Writes this instance in INI format to a file. - * - * @param file where to save to - * @throws IOException at an I/O problem - */ - public void save(File file) throws IOException { - OutputStream out = new FileOutputStream(file); - save(out); - out.close(); - } - - /** - * Writes this instance in INI format to an output stream. This method - * takes an OutputStream for maximum flexibility, internally - * it does of course use a writer for character based output. - * - * @param stream where to write - * @throws IOException at an I/O problem - */ - public void save(OutputStream stream) throws IOException { - save(new OutputStreamWriter(stream)); - } - - /** - * Writes this instance in INI format to an output stream writer. - * - * @param streamWriter where to write - * @throws IOException at an I/O problem - */ - public void save(OutputStreamWriter streamWriter) throws IOException { - Iterator it = this.sectionOrder.iterator(); - PrintWriter writer = new PrintWriter(streamWriter, true); - while (it.hasNext()) { - Section sect = getSection(it.next()); - writer.println(sect.header()); - sect.save(writer); - } - } - - /** - * Loads INI formatted input from a file into this instance, using the - * default character encoding. Everything in the file before the first - * section header is ignored. - * - * @param filename file to read from - * @throws IOException at an I/O problem - */ - public void load(String filename) throws IOException { - load(new File(filename)); - } - - /** - * Loads INI formatted input from a file into this instance, using the - * default character encoding. Everything in the file before the first - * section header is ignored. - * - * @param file file to read from - * @throws IOException at an I/O problem - */ - public void load(File file) throws IOException { - InputStream in = new FileInputStream(file); - load(in); - in.close(); - } - - /** - * Loads INI formatted input from a stream into this instance, using the - * default character encoding. This method takes an InputStream - * for maximum flexibility, internally it does use a reader (using the - * default character encoding) for character based input. Everything in the - * stream before the first section header is ignored. - * - * @param stream where to read from - * @throws IOException at an I/O problem - */ - public void load(InputStream stream) throws IOException { - load(new InputStreamReader(stream)); - } - - /** - * Loads INI formatted input from a stream reader into this instance. - * Everything in the stream before the first section header is ignored. - * - * @param streamReader where to read from - * @throws IOException at an I/O problem - */ - public void load(InputStreamReader streamReader) throws IOException { - BufferedReader reader = new BufferedReader(streamReader); - String curSection = null; - String line = null; - - while (reader.ready()) { - line = reader.readLine().trim(); - if (line.length() > 0 && line.charAt(0) == Section.HEADER_START) { - int endIndex = line.indexOf(Section.HEADER_END); - if (endIndex >= 0) { - curSection = line.substring(1, endIndex); - addSection(curSection); - } - } - if (curSection != null) { - Section sect = getSection(curSection); - sect.load(reader); - } - } - } - - /** - * Returns a section by name or null if not found. - * - * @param name the section's name - * @return the section - */ - private Section getSection(String name) { - return sections.get(normSection(name)); - } - - /** - * Normalizes an arbitrary string for use as a section name. Currently - * only makes the string lower-case (provided this instance isn't case- - * sensitive) and trims leading and trailing white space. Note that - * normalization isn't enforced by the Section class. - * - * @param name the string to be used as section name - * @return a normalized section name - */ - private String normSection(String name) { - if (!this.isCaseSensitive) { - name = name.toLowerCase(); - } - return name.trim(); - } - - private static String[] toStringArray(Collection coll) { - Object[] objArray = coll.toArray(); - String[] strArray = new String[objArray.length]; - for (int i = 0; i < objArray.length; i++) { - strArray[i] = (String)objArray[i]; - } - return strArray; - } - - /** - * Loads, edits and saves a section of an INI-style configuration file. This - * class does actually belong to the internals of {@link IniEditor} and - * should rarely ever be used directly. It's exposed because it can be - * useful for plain, section-less configuration files (Java-style - * properties, for example). - */ - public static class Section { - - private String name; - private Map options; - private List lines; - private char[] optionDelims; - private char[] optionDelimsSorted; - private char[] commentDelims; - private char[] commentDelimsSorted; - private boolean isCaseSensitive; - private OptionFormat optionFormat; - - private static final char[] DEFAULT_OPTION_DELIMS - = new char[] {'=', ':'}; - private static final char[] DEFAULT_COMMENT_DELIMS - = new char[] {'#', ';'}; - private static final char[] OPTION_DELIMS_WHITESPACE - = new char[] {' ', '\t'}; - private static final boolean DEFAULT_CASE_SENSITIVITY = false; - public static final String DEFAULT_OPTION_FORMAT = "%s %s %s"; - - public static final char HEADER_START = '['; - public static final char HEADER_END = ']'; - private static final int NAME_MAXLENGTH = 1024; - private static final char[] INVALID_NAME_CHARS = {HEADER_START, HEADER_END}; - - /** - * Constructs a new section. - * - * @param name the section's name - * @throws IllegalArgumentException the section's name is illegal - */ - public Section(String name) { - this(name, null); - } - - /** - * Constructs a new section, specifying case-sensitivity. - * - * @param name the section's name - * @param isCaseSensitive option names are case-sensitive if this is true - * @throws IllegalArgumentException the section's name is illegal - */ - public Section(String name, boolean isCaseSensitive) { - this(name, null, isCaseSensitive); - } - - /** - * Constructs a new section, defining comment delimiters. - * - * @param name the section's name - * @param delims an array of characters to be recognized as starters of - * comment lines; the first of them will be used for newly created - * comments - * @throws IllegalArgumentException the section's name is illegal - */ - public Section(String name, char[] delims) { - this(name, delims, DEFAULT_CASE_SENSITIVITY); - } - - /** - * Constructs a new section, defining comment delimiters. - * - * @param name the section's name - * @param delims an array of characters to be recognized as starters of - * comment lines; the first of them will be used for newly created - * comments - * @param isCaseSensitive option names are case-sensitive if this is true - * @throws IllegalArgumentException the section's name is illegal - */ - public Section(String name, char[] delims, boolean isCaseSensitive) { - if (!validName(name)) { - throw new IllegalArgumentException("Illegal section name:" + name); - } - this.name = name; - this.isCaseSensitive = isCaseSensitive; - this.options = new HashMap(); - this.lines = new LinkedList(); - this.optionDelims = DEFAULT_OPTION_DELIMS; - this.commentDelims = (delims == null ? DEFAULT_COMMENT_DELIMS : delims); - this.optionFormat = new OptionFormat(DEFAULT_OPTION_FORMAT); - // sorting so we can later use binary search - this.optionDelimsSorted = new char[this.optionDelims.length]; - System.arraycopy(this.optionDelims, 0, this.optionDelimsSorted, 0, this.optionDelims.length); - this.commentDelimsSorted = new char[this.commentDelims.length]; - System.arraycopy(this.commentDelims, 0, this.commentDelimsSorted, 0, this.commentDelims.length); - Arrays.sort(this.optionDelimsSorted); - Arrays.sort(this.commentDelimsSorted); - } - - /** - * Sets the option format for this section to the given string. Options - * in this section will be rendered according to the given format - * string. The string must contain %s three times, these - * will be replaced with the option name, the option separator and the - * option value in this order. Literal percentage signs must be escaped - * by preceding them with another percentage sign (i.e., %% - * corresponds to one percentage sign). The default format string is - * "%s %s %s". - * - * Option formats may look like format strings as supported by Java 1.5, - * but the string is in fact parsed in a custom fashion to guarantee - * backwards compatibility. So don't try clever stuff like using format - * conversion types other than %s. - * - * @param formatString a format string, containing %s - * exactly three times - * @throws IllegalArgumentException if the format string is illegal - */ - public void setOptionFormatString(String formatString) { - this.setOptionFormat(new OptionFormat(formatString)); - } - - /** - * Sets the option format for this section. Options will be rendered - * according to the given format when printed. - * - * @param format a compiled option format - */ - public void setOptionFormat(OptionFormat format) { - this.optionFormat = format; - } - - /** - * Returns the names of all options in this section. - * - * @return list of names of this section's options in - * original/insertion order - */ - public List optionNames() { - List optNames = new LinkedList(); - Iterator it = this.lines.iterator(); - while (it.hasNext()) { - Line line = it.next(); - if (line instanceof Option) { - optNames.add(((Option)line).name()); - } - } - return optNames; - } - - /** - * Checks whether a given option exists in this section. - * - * @param name the name of the option to test for - * @return true if the option exists in this section - */ - public boolean hasOption(String name) { - return this.options.containsKey(normOption(name)); - } - - /** - * Returns an option's value. - * - * @param option the name of the option - * @return the requested option's value or null if no - * option with the specified name exists - */ - public String get(String option) { - String normed = normOption(option); - if (hasOption(normed)) { - return getOption(normed).value(); - } - return null; - } - - /** - * Sets an option's value and creates the option if it doesn't exist. - * - * @param option the option's name - * @param value the option's value - * @throws IllegalArgumentException the option name is illegal, - * ie contains a '=' character or consists only of white space - */ - public void set(String option, String value) { - set(option, value, this.optionDelims[0]); - } - - /** - * Sets an option's value and creates the option if it doesn't exist. - * - * @param option the option's name - * @param value the option's value - * @param delim the delimiter between name and value for this option - * @throws IllegalArgumentException the option name is illegal, - * ie contains a '=' character or consists only of white space - */ - public void set(String option, String value, char delim) { - String normed = normOption(option); - if (hasOption(normed)) { - getOption(normed).set(value); - } - else { - // Option constructor might throw IllegalArgumentException - Option opt = new Option(normed, value, delim, this.optionFormat); - this.options.put(normed, opt); - this.lines.add(opt); - } - } - - /** - * Removes an option if it exists. - * - * @param option the name of the option - * @return true if the option was actually removed - */ - public boolean remove(String option) { - String normed = normOption(option); - if (hasOption(normed)) { - this.lines.remove(getOption(normed)); - this.options.remove(normed); - return true; - } - else { - return false; - } - } - - /** - * Adds a comment line to the end of this section. A comment spanning - * several lines (ie with line breaks) will be split up, one comment - * line for each line. - * - * @param comment the comment - */ - public void addComment(String comment) { - addComment(comment, this.commentDelims[0]); - } - - /** - * Adds a comment line to the end of this section. A comment spanning - * several lines (ie with line breaks) will be split up, one comment - * line for each line. - * - * @param comment the comment - * @param delim the delimiter used to mark the start of this comment - */ - public void addComment(String comment, char delim) { - StringTokenizer st = new StringTokenizer(comment.trim(), NEWLINE_CHARS); - while (st.hasMoreTokens()) { - this.lines.add(new Comment(st.nextToken(), delim)); - } - } - private static final String NEWLINE_CHARS = "\n\r"; - - /** - * Adds a blank line to the end of this section. - */ - public void addBlankLine() { - this.lines.add(BLANK_LINE); - } - - /** - * Loads options from a reader into this instance. Will read from the - * stream until it hits a section header, ie a '[' character, and resets - * the reader to point to this character. - * - * @param reader where to read from - * @throws IOException at an I/O problem - */ - public void load(BufferedReader reader) throws IOException { - while (reader.ready()) { - reader.mark(NAME_MAXLENGTH); - String line = reader.readLine().trim(); - - // Check for section header - if (line.length() > 0 && line.charAt(0) == HEADER_START) { - reader.reset(); - return; - } - - int delimIndex = -1; - // blank line - if (line.equals("")) { - this.addBlankLine(); - } - // comment line - else if ((delimIndex = Arrays.binarySearch(this.commentDelimsSorted, line.charAt(0))) >= 0) { - addComment(line.substring(1), this.commentDelimsSorted[delimIndex]); - } - // option line - else { - delimIndex = -1; - int delimNum = -1; - int lastSpaceIndex = -1; - for (int i = 0, l = line.length(); i < l && delimIndex < 0; i++) { - delimNum = Arrays.binarySearch(this.optionDelimsSorted, line.charAt(i)); - if (delimNum >= 0) { - delimIndex = i; - } - else { - boolean isSpace = Arrays.binarySearch( - this.OPTION_DELIMS_WHITESPACE, line.charAt(i)) >= 0; - if (!isSpace && lastSpaceIndex >= 0) { - break; - } - else if (isSpace) { - lastSpaceIndex = i; - } - } - } - // delimiter at start of line - if (delimIndex == 0) { - // XXX what's a man got to do? - } - // no delimiter found - else if (delimIndex < 0) { - if (lastSpaceIndex < 0) { - this.set(line, ""); - } - else { - this.set(line.substring(0, lastSpaceIndex) - , line.substring(lastSpaceIndex+1)); - } - } - // delimiter found - else { - this.set(line.substring(0, delimIndex) - , line.substring(delimIndex+1), line.charAt(delimIndex)); - } - } - } - } - - /** - * Prints this section to a print writer. - * - * @param writer where to write - * @throws IOException at an I/O problem - */ - public void save(PrintWriter writer) throws IOException { - Iterator it = this.lines.iterator(); - while (it.hasNext()) { - writer.println(it.next().toString()); - } - if (writer.checkError()) { - throw new IOException(); - } - } - - /** - * Returns an actual Option instance. - * - * @param option the name of the option, assumed to be normed already (!) - * @return the requested Option instance - * @throws NullPointerException if no option with the specified name exists - */ - private Option getOption(String name) { - return this.options.get(name); - } - - /** - * Returns the bracketed header of this section as appearing in an - * actual INI file. - * - * @return the section's name in brackets - */ - private String header() { - return HEADER_START + this.name + HEADER_END; - } - - /** - * Checks a string for validity as a section name. It can't contain the - * characters '[' and ']'. An empty string or one consisting only of - * white space isn't allowed either. - * - * @param name the name to validate - * @return true if the name validates as a section name - */ - private static boolean validName(String name) { - if (name.trim().equals("")) { - return false; - } - for (int i = 0; i < INVALID_NAME_CHARS.length; i++) { - if (name.indexOf(INVALID_NAME_CHARS[i]) >= 0) { - return false; - } - } - return true; - } - - /** - * Normalizes an arbitrary string for use as an option name, ie makes - * it lower-case (provided this section isn't case-sensitive) and trims - * leading and trailing white space. - * - * @param name the string to be used as option name - * @return a normalized option name - */ - private String normOption(String name) { - if (!this.isCaseSensitive) { - name = name.toLowerCase(); - } - return name.trim(); - } - - } - - private interface Line { - public String toString(); - } - - private static final Line BLANK_LINE = new Line() { - public String toString() { return ""; } - }; - - private static class Option implements Line { - - private String name; - private String value; - private char separator; - private OptionFormat format; - - private static final String ILLEGAL_VALUE_CHARS = "\n\r"; - - public Option(String name, String value, char separator, OptionFormat format) { - if (!validName(name, separator)) { - throw new IllegalArgumentException("Illegal option name:" + name); - } - this.name = name; - this.separator = separator; - this.format = format; - set(value); - } - - public String name() { - return this.name; - } - - public String value() { - return this.value; - } - - public void set(String value) { - if (value == null) { - this.value = value; - } - else { - StringTokenizer st = new StringTokenizer(value.trim(), - ILLEGAL_VALUE_CHARS); - StringBuffer sb = new StringBuffer(); - // XXX this might not be particularly efficient - while (st.hasMoreTokens()) { - sb.append(st.nextToken()); - } - this.value = sb.toString(); - } - } - - public String toString() { - return this.format.format(this.name, this.value, this.separator); - } - - private static boolean validName(String name, char separator) { - if (name.trim().equals("")) { - return false; - } - if (name.indexOf(separator) >= 0) { - return false; - } - return true; - } - - } - - private static class Comment implements Line { - - private String comment; - private char delimiter; - - private static final char DEFAULT_DELIMITER = '#'; - - public Comment(String comment) { - this(comment, DEFAULT_DELIMITER); - } - - public Comment(String comment, char delimiter) { - this.comment = comment.trim(); - this.delimiter = delimiter; - } - - public String toString() { - return this.delimiter + " " + this.comment; - } - - } - - private static class OptionFormat { - - private static final int EXPECTED_TOKENS = 4; - - private String[] formatTokens; - - public OptionFormat(String formatString) { - this.formatTokens = this.compileFormat(formatString); - } - - public String format(String name, String value, char separator) { - String[] t = this.formatTokens; - return t[0] + name + t[1] + separator + t[2] + value + t[3]; - } - - private String[] compileFormat(String formatString) { - String[] tokens = {"", "", "", ""}; - int tokenCount = 0; - boolean seenPercent = false; - StringBuffer token = new StringBuffer(); - for (int i = 0; i < formatString.length(); i++) { - switch (formatString.charAt(i)) { - case '%': - if (seenPercent) { - token.append("%"); - seenPercent = false; - } - else { - seenPercent = true; - } - break; - case 's': - if (seenPercent) { - if (tokenCount >= EXPECTED_TOKENS) { - throw new IllegalArgumentException( - "Illegal option format. Too many %s placeholders."); - } - tokens[tokenCount] = token.toString(); - tokenCount++; - token = new StringBuffer(); - seenPercent = false; - } - else { - token.append("s"); - } - break; - default: - if (seenPercent) { - throw new IllegalArgumentException( - "Illegal option format. Unknown format specifier."); - } - token.append(formatString.charAt(i)); - break; - } - } - if (tokenCount != EXPECTED_TOKENS - 1) { - throw new IllegalArgumentException( - "Illegal option format. Not enough %s placeholders."); - } - tokens[tokenCount] = token.toString(); - return tokens; - } - - } - - /** - * Thrown when an non-existent section is addressed. - */ - public static class NoSuchSectionException extends RuntimeException { - public NoSuchSectionException() { super(); } - public NoSuchSectionException(String msg) { super(msg); } - } - -} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 64b44b1..5cf69bc 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -58,7 +58,7 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08e0af2..607744a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,13 +18,13 @@ Graceful shutdown in progress Already stopped - Application initializing... - Application starting... + Application initializing… + Application starting… Loaded JNI libraries Application started Start failed Application stopped - Application stopping... + Application stopping… remaining OK @@ -50,7 +50,7 @@ Planned shutdown canceled - Reloading tunnels config... + Reloading tunnels config… Common settings Tunnels @@ -60,5 +60,6 @@ Tunnels management Delete tunnel + Error: i2pd not running diff --git a/binary/jni/README.md b/binary/jni/README.md new file mode 100644 index 0000000..7ecd306 --- /dev/null +++ b/binary/jni/README.md @@ -0,0 +1,20 @@ +# Android command line executable build instructions + +1. Install ndk 23.2.8568313 into some directory, usually `ANDROID_SDK/ndk/23.2.8568313`. + +2. Install cmake 3.19 or newer, e.g. for Unix: + +```sh +wget -t0 https://github.com/Kitware/CMake/releases/download/v3.28.0/cmake-3.28.0.tar.gz && tar xzvf cmake-3.28.0.tar.gz && cd cmake-3.28.0 && ./configure && make -j$(nproc) && sudo make install +``` + +3. In the current working directory (the one with Android.mk and Application.mk), run: + +```bash +# E.g, export ANDROID_NDK_HOME=/opt/android-sdk/ndk/23.2.8568313 +export ANDROID_NDK_HOME=your_ndk_directory +./build_all.sh +``` + +Then, `../libs` will be populated with build executable files +(that are renamed to `*.so` for Gradle to put them into the APK). diff --git a/binary/jni/build_all.sh b/binary/jni/build_all.sh new file mode 100755 index 0000000..bc693d5 --- /dev/null +++ b/binary/jni/build_all.sh @@ -0,0 +1,9 @@ +if [ -z "$ANDROID_NDK_HOME" -a "$ANDROID_NDK_HOME" == "" ]; then + echo -e "\033[31mFailed! ANDROID_NDK_HOME is empty. Run 'export ANDROID_NDK_HOME=[PATH_TO_NDK]'\033[0m" + exit 1 +fi +./build_boost.sh +./build_miniupnpc.sh +./build_openssl.sh +export NDK_MODULE_PATH=`pwd` && export NDK_PROJECT_PATH=`pwd`/.. && ./ndkbuild-wrapper.sh V=1 NDK_LOG=1 -j`nproc` +echo "$0 completed." diff --git a/binary/jni/ndkbuild-wrapper.sh b/binary/jni/ndkbuild-wrapper.sh new file mode 100755 index 0000000..3868931 --- /dev/null +++ b/binary/jni/ndkbuild-wrapper.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +cd $NDK_PROJECT_PATH/jni +$ANDROID_NDK_HOME/ndk-build $* +mkdir $NDK_PROJECT_PATH/libs +cd $NDK_PROJECT_PATH/libs +for f in $(ls .); +do + mv $f/i2pd $f/libi2pd.so +done \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1debed0..31e1e34 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ +#Tue Apr 02 16:43:23 IRKT 2024 distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionSha256Sum=8cc27038d5dbd815759851ba53e70cf62e481b87494cc97cfd97982ada5ba634 distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip -zipStoreBase=GRADLE_USER_HOME +distributionPath=wrapper/dists zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME