From fd04d7070df9c899a411a3f60f267e22db039c65 Mon Sep 17 00:00:00 2001 From: Aleksander Undisclosed Yvarov Date: Sun, 28 Feb 2021 21:33:39 +0300 Subject: [PATCH] Enable/Disable button; Toast on webconsole if not enabled; i2pdpath;on boot autostart;getDataPath/iniEditor preinit for setting --- app/jni/DaemonAndroid.cpp | 6 + app/jni/DaemonAndroid.h | 2 + app/jni/i2pd_android.cpp | 4 + app/jni/org_purplei2p_i2pd_I2PD_JNI.h | 3 + app/src/main/AndroidManifest.xml | 9 + .../org/purplei2p/i2pd/DaemonWrapper.java | 65 +- .../java/org/purplei2p/i2pd/I2PDActivity.java | 39 +- .../java/org/purplei2p/i2pd/I2PD_JNI.java | 2 + .../purplei2p/i2pd/iniedotr/IniEditor.java | 1182 +++++++++++++++++ .../i2pd/receivers/BootUpReceiver.java | 19 + app/src/main/res/layout/activity_main.xml | 26 +- gradle.properties | 3 +- 12 files changed, 1328 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java create mode 100644 app/src/main/java/org/purplei2p/i2pd/receivers/BootUpReceiver.java diff --git a/app/jni/DaemonAndroid.cpp b/app/jni/DaemonAndroid.cpp index 39c06ea..12caa62 100644 --- a/app/jni/DaemonAndroid.cpp +++ b/app/jni/DaemonAndroid.cpp @@ -218,5 +218,11 @@ namespace android { dataDir = jdataDir; } + + std::string + GetDataDir(void) + { + return dataDir; + } } } diff --git a/app/jni/DaemonAndroid.h b/app/jni/DaemonAndroid.h index 912f6f4..41a7ca5 100644 --- a/app/jni/DaemonAndroid.h +++ b/app/jni/DaemonAndroid.h @@ -52,6 +52,8 @@ namespace android // set datadir received from jni void SetDataDir(std::string jdataDir); + // get datadir + std::string GetDataDir(void); /* class Worker : public QObject { diff --git a/app/jni/i2pd_android.cpp b/app/jni/i2pd_android.cpp index 8103895..2aa95a9 100644 --- a/app/jni/i2pd_android.cpp +++ b/app/jni/i2pd_android.cpp @@ -56,6 +56,10 @@ JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_startDaemon (JNIEnv *env, jclass clazz) { return env->NewStringUTF(i2p::android::start().c_str()); } +JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_getDataDir + (JNIEnv *env, jclass clazz) { + return env->NewStringUTF( i2p::android::GetDataDir().c_str() ); +} JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_stopDaemon (JNIEnv *env, jclass clazz) { diff --git a/app/jni/org_purplei2p_i2pd_I2PD_JNI.h b/app/jni/org_purplei2p_i2pd_I2PD_JNI.h index 43153e5..728f3ac 100644 --- a/app/jni/org_purplei2p_i2pd_I2PD_JNI.h +++ b/app/jni/org_purplei2p_i2pd_I2PD_JNI.h @@ -26,6 +26,9 @@ JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_getABICompiledWith JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_startDaemon (JNIEnv *, jclass); +JNIEXPORT jstring JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_getDataDir + (JNIEnv *, jclass); + JNIEXPORT void JNICALL Java_org_purplei2p_i2pd_I2PD_1JNI_stopDaemon (JNIEnv *, jclass); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd7ff0e..92078a9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,8 @@ + + + + + + + + diff --git a/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java index 5d39cb5..0d63d62 100644 --- a/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java +++ b/app/src/main/java/org/purplei2p/i2pd/DaemonWrapper.java @@ -24,6 +24,7 @@ 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; @@ -118,31 +119,7 @@ public class DaemonWrapper { this.assetManager = assetManager; this.connectivityManager = connectivityManager; setState(State.starting); - new Thread(() -> { - try { - processAssets(); - I2PD_JNI.loadLibraries(); - setState(State.jniLibraryLoaded); - registerNetworkCallback(); - } catch (Throwable tr) { - lastThrowable = tr; - setState(State.startFailed); - return; - } - try { - synchronized (DaemonWrapper.this) { - I2PD_JNI.setDataDir(Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd"); - daemonStartResult = I2PD_JNI.startDaemon(); - if ("ok".equals(daemonStartResult)) { - setState(State.startedOkay); - } else - setState(State.startFailed); - } - } catch (Throwable tr) { - lastThrowable = tr; - setState(State.startFailed); - } - }, "i2pdDaemonStart").start(); + startDaemon(); } private Throwable lastThrowable; @@ -167,6 +144,16 @@ public class DaemonWrapper { return daemonStartResult; } + public 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(); } @@ -182,6 +169,34 @@ public class DaemonWrapper { setState(State.stopped); } } + public synchronized void startDaemon() { + if( getState() != State.stopped && getState() != State.starting ) return; + 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(i2pdpath);//(Environment.getExternalStorageDirectory().getAbsolutePath() + "/i2pd"); + daemonStartResult = I2PD_JNI.startDaemon(); + if ("ok".equals(daemonStartResult)) { + setState(State.startedOkay); + } else + setState(State.startFailed); + } + } catch (Throwable tr) { + lastThrowable = tr; + setState(State.startFailed); + } + }, "i2pdDaemonStart").start(); + } private void processAssets() { if (!assetsCopied) { diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java index bbb3a1f..46e9005 100644 --- a/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java +++ b/app/src/main/java/org/purplei2p/i2pd/I2PDActivity.java @@ -27,6 +27,8 @@ import android.provider.Settings; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.View; +import android.widget.Button; import android.widget.TextView; import android.widget.Toast; @@ -35,12 +37,17 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import static android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS; +import static org.purplei2p.i2pd.DaemonWrapper.State.startedOkay; +import static org.purplei2p.i2pd.DaemonWrapper.State.starting; +import static org.purplei2p.i2pd.DaemonWrapper.State.stopped; public class I2PDActivity extends Activity { 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 Button enableButton; + private Button disableButton; private TextView textView; @@ -64,6 +71,9 @@ public class I2PDActivity extends Activity { return; } DaemonWrapper.State state = daemon.getState(); + if(state == startedOkay){ + disableButton.setVisibility(View.VISIBLE); + } 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)); @@ -92,9 +102,9 @@ public class I2PDActivity extends Activity { Log.i(TAG, "onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - + enableButton = findViewById(R.id.enableButton); + disableButton = findViewById(R.id.disableButton); textView = (TextView) findViewById(R.id.textView); - if (daemon == null) { ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); daemon = new DaemonWrapper(getAssets(), connectivityManager); @@ -103,7 +113,25 @@ public class I2PDActivity extends Activity { daemon.addStateChangeListener(daemonStateUpdatedListener); daemonStateUpdatedListener.daemonStateUpdate(DaemonWrapper.State.uninitialized, daemon.getState()); - + enableButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if( daemon.getState() != startedOkay && daemon.getState() != starting ){ + daemon.stopDaemon(); + daemon.startDaemon(); + disableButton.setVisibility(View.VISIBLE); + } + } + }); + disableButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if( daemon.getState() != stopped ){ + daemon.stopDaemon(); + disableButton.setVisibility(View.INVISIBLE); + } + } + }); // request permissions if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { @@ -266,7 +294,10 @@ public class I2PDActivity extends Activity { return true; case R.id.action_start_webview: - startActivity(new Intent(getApplicationContext(), WebConsoleActivity.class)); + if( daemon.getState() == startedOkay) + startActivity(new Intent(getApplicationContext(), WebConsoleActivity.class)); + else + Toast.makeText(this,"I2Pd not was started!", Toast.LENGTH_SHORT).show(); return true; } diff --git a/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java b/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java index 260138f..621d82a 100644 --- a/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java +++ b/app/src/main/java/org/purplei2p/i2pd/I2PD_JNI.java @@ -30,4 +30,6 @@ public class I2PD_JNI { //System.loadLibrary("c++_shared"); System.loadLibrary("i2pd"); } + + public static native String getDataDir(); } diff --git a/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java b/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java new file mode 100644 index 0000000..8bb2b4c --- /dev/null +++ b/app/src/main/java/org/purplei2p/i2pd/iniedotr/IniEditor.java @@ -0,0 +1,1182 @@ +/* + 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: + *

    + *
  • Line continuations (backslashes at the end of an option line) are not + * supported.
  • + *
  • No kind of character escaping is performed or recognized. Characters are + * read and written in in the default character encoding. If you want to use a + * different character encoding, use the {@link #load(InputStreamReader)} and + * {@link #save(OutputStreamWriter)} methods with a reader and writer tuned + * to the desired character encoding.
  • + *
  • As a consequence, option names may not contain option/value separators + * (normally '=', ':' and white space).
  • + *
+ * + * @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/java/org/purplei2p/i2pd/receivers/BootUpReceiver.java b/app/src/main/java/org/purplei2p/i2pd/receivers/BootUpReceiver.java new file mode 100644 index 0000000..08a0688 --- /dev/null +++ b/app/src/main/java/org/purplei2p/i2pd/receivers/BootUpReceiver.java @@ -0,0 +1,19 @@ +package org.purplei2p.i2pd.receivers; + + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.purplei2p.i2pd.I2PDPermsAskerActivity; + +public class BootUpReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + /* todo: disable the autostart? */ + Intent i = new Intent(context, I2PDPermsAskerActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + } +} + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index dd5823f..25901e5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,12 +5,30 @@ android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingBottom="@dimen/activity_vertical_margin" + android:background="#9C27B0" android:paddingLeft="@dimen/activity_horizontal_margin" - android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" tools:context="org.purplei2p.i2pd.I2PDActivity"> +