summaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/org/xapek/andiodine/FragmentList.java205
-rw-r--r--src/main/java/org/xapek/andiodine/FragmentStatus.java120
-rw-r--r--src/main/java/org/xapek/andiodine/IodineClient.java53
-rw-r--r--src/main/java/org/xapek/andiodine/IodineMain.java112
-rw-r--r--src/main/java/org/xapek/andiodine/IodinePref.java112
-rw-r--r--src/main/java/org/xapek/andiodine/IodineVpnService.java350
-rw-r--r--src/main/java/org/xapek/andiodine/config/ConfigDatabase.java124
-rw-r--r--src/main/java/org/xapek/andiodine/config/IodineConfiguration.java132
-rw-r--r--src/main/java/org/xapek/andiodine/preferences/AbstractPreference.java49
-rw-r--r--src/main/java/org/xapek/andiodine/preferences/BooleanPreference.java33
-rw-r--r--src/main/java/org/xapek/andiodine/preferences/PreferenceActivity.java104
-rw-r--r--src/main/java/org/xapek/andiodine/preferences/SpinnerPreference.java47
-rw-r--r--src/main/java/org/xapek/andiodine/preferences/TextPreference.java35
13 files changed, 1476 insertions, 0 deletions
diff --git a/src/main/java/org/xapek/andiodine/FragmentList.java b/src/main/java/org/xapek/andiodine/FragmentList.java
new file mode 100644
index 0000000..10a0875
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/FragmentList.java
@@ -0,0 +1,205 @@
+package org.xapek.andiodine;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.xapek.andiodine.config.ConfigDatabase;
+import org.xapek.andiodine.config.IodineConfiguration;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class FragmentList extends Fragment {
+ public static final String TAG = "FRAGMENT_LIST";
+
+ private ListView mListView;
+ private ConfigDatabase mConfigDatabase;
+ private IodineConfiguration mSelectedConfiguration;
+ private IodineConfigurationAdapter mAdapter;
+
+ private static final int INTENT_REQUEST_CODE_PREPARE = 0;
+
+ private class IodineConfigurationAdapter extends BaseAdapter {
+ private List<IodineConfiguration> configurations;
+ private final Set<DataSetObserver> observers = new HashSet<DataSetObserver>();
+
+ public IodineConfigurationAdapter() {
+ reload();
+ }
+
+ @Override
+ public int getCount() {
+ return configurations.size();
+ }
+
+ @Override
+ public IodineConfiguration getItem(int position) {
+ return configurations.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final IodineConfiguration item = getItem(position);
+ View view = View.inflate(parent.getContext(), R.layout.configitem, null);
+ ((TextView) view.findViewById(R.id.configitem_text_name)).setText(item.getName());
+ ((TextView) view.findViewById(R.id.configitem_text_topdomain)).setText(item.getTopDomain());
+
+ view.findViewById(R.id.configitem_btn_manage)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FragmentList.this.vpnPreferences(item);
+ }
+ });
+
+ view.findViewById(R.id.configitem_layout_name)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FragmentList.this.vpnServiceConnect(item);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ observers.add(observer);
+ }
+
+ private void reload() {
+ this.configurations = mConfigDatabase.selectAll();
+ triggerOnChanged();
+ }
+
+ public void triggerOnChanged() {
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ observers.remove(observer);
+ }
+ }
+
+ private final BroadcastReceiver broadcastReceiverConfigurationChanged = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (IodinePref.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
+ // CONFIGURATION_CHANGED
+ mAdapter.reload();
+ }
+ }
+ };
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mConfigDatabase = new ConfigDatabase(getActivity());
+ mListView = (ListView) getActivity().findViewById(R.id.list_view);
+ mAdapter = new IodineConfigurationAdapter();
+ mListView.setAdapter(mAdapter);
+
+ IntentFilter intentFilterConfigurationChanged = new IntentFilter();
+ intentFilterConfigurationChanged.addAction(IodinePref.ACTION_CONFIGURATION_CHANGED);
+ getActivity().registerReceiver(broadcastReceiverConfigurationChanged, intentFilterConfigurationChanged);
+
+ setHasOptionsMenu(true); //activate onCreateOptionsMenu
+ }
+
+ @Override
+ public void onDestroy() {
+ getActivity().unregisterReceiver(broadcastReceiverConfigurationChanged);
+ mConfigDatabase.close();
+ super.onDestroy();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_list, null);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.fragment_list, menu);
+ }
+
+ private void vpnPreferences(IodineConfiguration item) {
+ Intent intent = new Intent(getActivity(), IodinePref.class);
+ intent.putExtra(IodinePref.EXTRA_CONFIGURATION_ID, item.getId());
+ startActivity(intent);
+ }
+
+ private void vpnServiceConnect(IodineConfiguration configuration) {
+ Intent intent = IodineVpnService.prepare(getActivity());
+ mSelectedConfiguration = configuration;
+ if (intent != null) {
+ // Ask for permission
+ intent.putExtra(IodineVpnService.EXTRA_CONFIGURATION_ID, configuration.getId());
+ startActivityForResult(intent, INTENT_REQUEST_CODE_PREPARE);
+ } else {
+ // Permission already granted
+ new AlertDialog.Builder(getActivity()) //
+ .setTitle(R.string.warning) //
+ .setCancelable(true) //
+ .setMessage(getString(R.string.main_create_tunnel, configuration.getName()))
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ vpnServiceConnect2(mSelectedConfiguration);
+ }
+ }) //
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ }) //
+ .create() //
+ .show();
+
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == INTENT_REQUEST_CODE_PREPARE && resultCode == Activity.RESULT_OK) {
+ vpnServiceConnect2(mSelectedConfiguration);
+ }
+ }
+
+ private void vpnServiceConnect2(IodineConfiguration configuration) {
+ Log.d(TAG, "Call VPN Service for configuration: " + configuration.getId());
+ Intent intent = new Intent(IodineVpnService.ACTION_CONTROL_CONNECT);
+ intent.putExtra(IodineVpnService.EXTRA_CONFIGURATION_ID, configuration.getId());
+ getActivity().sendBroadcast(intent);
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/FragmentStatus.java b/src/main/java/org/xapek/andiodine/FragmentStatus.java
new file mode 100644
index 0000000..c1f709a
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/FragmentStatus.java
@@ -0,0 +1,120 @@
+package org.xapek.andiodine;
+
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+public class FragmentStatus extends Fragment {
+ public static final String TAG = "FRAGMENT_STATUS";
+
+ private TextView mStatus;
+ private TextView mLogmessages;
+ private ScrollView mScrollview;
+ private Button mClose;
+
+ private final BroadcastReceiver broadcastReceiverStatusUpdates = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Got intent: " + intent);
+ if (IodineVpnService.ACTION_STATUS_ERROR.equals(intent.getAction())) {
+ final TextView message = new TextView(context);
+ final String stringMessage = intent.getStringExtra(IodineVpnService.EXTRA_ERROR_MESSAGE);
+ final SpannableString s = new SpannableString(stringMessage);
+ Linkify.addLinks(s, Linkify.WEB_URLS);
+ message.setText(s);
+ message.setMovementMethod(LinkMovementMethod.getInstance());
+ new AlertDialog.Builder(FragmentStatus.this.getActivity())//
+ .setIcon(R.drawable.error) //
+ .setTitle("Error") //
+ .setView(message)
+ .create() //
+ .show();
+ } else if (IodineVpnService.ACTION_STATUS_CONNECT.equals(intent.getAction())) {
+ mStatus.setText("Connect");
+ } else if (IodineVpnService.ACTION_STATUS_CONNECTED.equals(intent.getAction())) {
+ mStatus.setText("Connected: " + IodineClient.getIp() + '/'
+ + IodineClient.getNetbits() + " MTU: "
+ + IodineClient.getMtu() + '\n');
+ } else if (IodineVpnService.ACTION_STATUS_DISCONNECT.equals(intent.getAction())) {
+ mStatus.setText("Disconnect");
+ }
+ }
+ };
+
+ private final BroadcastReceiver broadcastReceiverLogMessages = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (IodineClient.ACTION_LOG_MESSAGE.equals(intent.getAction())) {
+ final String newLogEntry = intent.getStringExtra(IodineClient.EXTRA_MESSAGE);
+ if (!".".equals(newLogEntry)) // Suppress newline for progress indicator'.'
+ mLogmessages.append("\n");
+ mLogmessages.append(newLogEntry);
+ mScrollview.fullScroll(View.FOCUS_DOWN);
+ }
+ }
+ };
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mStatus = (TextView) getActivity().findViewById(R.id.status_message);
+ mLogmessages = (TextView) getActivity().findViewById(R.id.status_logmessages);
+ mScrollview = (ScrollView) getActivity().findViewById(R.id.status_scrollview);
+ mClose = (Button) getActivity().findViewById(R.id.status_cancel);
+ mClose.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ requestDisconnect();
+ }
+ });
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ IntentFilter intentFilterStatusUpdates = new IntentFilter();
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_CONNECT);
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_CONNECTED);
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_DISCONNECT);
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_ERROR);
+ getActivity().registerReceiver(broadcastReceiverStatusUpdates, intentFilterStatusUpdates);
+
+ Intent intent = new Intent(IodineVpnService.ACTION_CONTROL_UPDATE);
+ getActivity().sendBroadcast(intent);
+
+ IntentFilter intentFilterLogMessages = new IntentFilter();
+ intentFilterLogMessages.addAction(IodineClient.ACTION_LOG_MESSAGE);
+ getActivity().registerReceiver(broadcastReceiverLogMessages, intentFilterLogMessages);
+ }
+
+ @Override
+ public void onDestroy() {
+ getActivity().unregisterReceiver(broadcastReceiverStatusUpdates);
+ getActivity().unregisterReceiver(broadcastReceiverLogMessages);
+ super.onDestroy();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_status, null);
+ }
+
+ private void requestDisconnect() {
+ Intent intent = new Intent(IodineVpnService.ACTION_CONTROL_DISCONNECT);
+ getActivity().sendBroadcast(intent);
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/IodineClient.java b/src/main/java/org/xapek/andiodine/IodineClient.java
new file mode 100644
index 0000000..37178b5
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/IodineClient.java
@@ -0,0 +1,53 @@
+package org.xapek.andiodine;
+
+import android.content.Intent;
+import android.util.Log;
+
+public class IodineClient {
+ public static final String TAG = "NATIVE";
+
+ public static native int getDnsFd();
+
+ public static native int connect(String nameserv_addr, String topdomain, boolean raw_mode, boolean lazy_mode,
+ String password);
+
+ public static native String getIp();
+
+ public static native String getRemoteIp();
+
+ public static native int getNetbits();
+
+ public static native int getMtu();
+
+ public static native int tunnel(int fd);
+
+ public static native void tunnelInterrupt();
+
+ public static native String getPropertyNetDns1();
+
+ /**
+ * Intent to distribute logmessages from native code
+ * LOG_MESSAGE(EXTRA_MESSAGE)
+ */
+ public static final String ACTION_LOG_MESSAGE = "org.xapek.andiodine.IodineClient.ACTION_LOG_MESSAGE";
+
+ public static final String EXTRA_MESSAGE = "message";
+
+ @SuppressWarnings("UnusedDeclaration")
+ public static void log_callback(String message) {
+ Intent intent = new Intent(ACTION_LOG_MESSAGE);
+
+ intent.putExtra(EXTRA_MESSAGE, message);
+ if (IodineVpnService.instance != null) {
+ IodineVpnService.instance.sendBroadcast(intent);
+ } else {
+ Log.d(TAG, "No VPNService running, cannot broadcast native message");
+ }
+
+ }
+
+ static {
+ System.loadLibrary("iodine-client");
+ Log.d(TAG, "Native Library iodine-client loaded");
+ }
+} \ No newline at end of file
diff --git a/src/main/java/org/xapek/andiodine/IodineMain.java b/src/main/java/org/xapek/andiodine/IodineMain.java
new file mode 100644
index 0000000..f631c1d
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/IodineMain.java
@@ -0,0 +1,112 @@
+package org.xapek.andiodine;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import org.xapek.andiodine.config.ConfigDatabase;
+
+import java.util.Scanner;
+
+public class IodineMain extends Activity {
+ private static final String TAG = "MAIN";
+ private ConfigDatabase mConfigDatabase;
+
+ private final FragmentList fragmentList = new FragmentList();
+ private final FragmentStatus fragmentStatus = new FragmentStatus();
+
+ private final BroadcastReceiver broadcastReceiverStatusUpdates = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Got intent: " + intent);
+ if (IodineVpnService.ACTION_STATUS_ERROR.equals(intent.getAction())) {
+ // Switch to List of Configurations Fragment
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.replace(R.id.main_fragment_status, fragmentList);
+ ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ ft.commit();
+ } else if (IodineVpnService.ACTION_STATUS_IDLE.equals(intent.getAction())) {
+ // Switch to List of Configurations Fragment
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.replace(R.id.main_fragment_status, fragmentList);
+ ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ ft.commit();
+ } else if (IodineVpnService.ACTION_STATUS_CONNECT.equals(intent.getAction())
+ || IodineVpnService.ACTION_STATUS_CONNECTED.equals(intent.getAction())) {
+ // Switch to Status Fragment
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.replace(R.id.main_fragment_status, fragmentStatus);
+ ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ ft.commit();
+ }
+ }
+ };
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ mConfigDatabase = new ConfigDatabase(this);
+
+ startService(new Intent(this, IodineVpnService.class));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ IntentFilter intentFilterStatusUpdates = new IntentFilter();
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_CONNECT);
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_CONNECTED);
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_ERROR);
+ intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_IDLE);
+ registerReceiver(broadcastReceiverStatusUpdates, intentFilterStatusUpdates);
+
+ Log.d(TAG, "Request CONTROL_UPDATE");
+ sendBroadcast(new Intent(IodineVpnService.ACTION_CONTROL_UPDATE));
+ }
+
+ @Override
+ protected void onPause() {
+ unregisterReceiver(broadcastReceiverStatusUpdates);
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ mConfigDatabase.close();
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.menu_main_about) {
+ Scanner scanner = new Scanner(getResources().openRawResource(R.raw.license));
+ scanner.useDelimiter("\\A");
+ new AlertDialog.Builder(IodineMain.this)//
+ .setMessage(scanner.next()) //
+ .setCancelable(true)//
+ .create() //
+ .show();
+ scanner.close();
+ } else if (item.getItemId() == R.id.menu_main_add) {
+ startActivity(new Intent(this, IodinePref.class));
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/IodinePref.java b/src/main/java/org/xapek/andiodine/IodinePref.java
new file mode 100644
index 0000000..cc21b59
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/IodinePref.java
@@ -0,0 +1,112 @@
+package org.xapek.andiodine;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import org.xapek.andiodine.config.ConfigDatabase;
+import org.xapek.andiodine.config.IodineConfiguration;
+import org.xapek.andiodine.config.IodineConfiguration.NameserverMode;
+import org.xapek.andiodine.config.IodineConfiguration.RequestType;
+
+public class IodinePref extends org.xapek.andiodine.preferences.PreferenceActivity {
+ public static final String EXTRA_CONFIGURATION_ID = "uuid";
+ public static final String ACTION_CONFIGURATION_CHANGED = "org.xapek.andiodine.preferences.PreferenceActivity.CONFIGURATION_CHANGED";
+
+ private static final String TAG = "PREF";
+
+ private ConfigDatabase mConfigDatabase;
+
+ private IodineConfiguration mIodineConfiguration;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mConfigDatabase = new ConfigDatabase(this);
+ Long configurationId = getIntent().getLongExtra(EXTRA_CONFIGURATION_ID, -1);
+ if (configurationId == null || configurationId == -1) {
+ // Configuration ID is empty; create new configuration
+ mIodineConfiguration = new IodineConfiguration();
+ } else {
+ mIodineConfiguration = mConfigDatabase.selectById(configurationId);
+ if (mIodineConfiguration == null) {
+ Log.e(TAG, "No configuration with uuid: " + configurationId + " found");
+ finish();
+ }
+ }
+ setContentValues(mIodineConfiguration.getContentValues());
+
+ // Name
+ addPreference(ConfigDatabase.COLUMN_CONF_NAME, "Name", R.string.pref_help_name, "New Connection");
+ // Topdomain
+ addPreference(ConfigDatabase.COLUMN_CONF_TOP_DOMAIN, "Tunnel Topdomain", R.string.pref_help_topdomain,
+ "tun.example.com");
+ // Password
+ addPreference(ConfigDatabase.COLUMN_CONF_PASSWORD, "Password", R.string.pref_help_password, "");
+ // Tunnel Nameserver
+ addPreference(ConfigDatabase.COLUMN_CONF_TUNNEL_NAMESERVER, "Tunnel Nameserver (or empty)",
+ R.string.pref_help_tunnel_nameserver, "");
+ // Nameserver Mode
+ String[] nameserverModes = new String[NameserverMode.values().length];
+ for (int i = 0; i < NameserverMode.values().length; i++) {
+ nameserverModes[i] = NameserverMode.values()[i].name();
+ }
+ addPreference(ConfigDatabase.COLUMN_CONF_NAMESERVER_MODE, "Nameserver Mode",
+ R.string.pref_help_nameserver_mode, nameserverModes, NameserverMode.LEAVE_DEFAULT.name());
+ // Nameserver
+ addPreference(ConfigDatabase.COLUMN_CONF_NAMESERVER, "Nameserver", R.string.pref_help_nameserver, "");
+ // Request Type
+ String[] requestTypes = new String[RequestType.values().length];
+ for (int i = 0; i < RequestType.values().length; i++) {
+ requestTypes[i] = RequestType.values()[i].name();
+ }
+ addPreference(ConfigDatabase.COLUMN_CONF_REQUEST_TYPE, "Request Type", R.string.pref_help_request_type,
+ requestTypes, RequestType.AUTODETECT.name());
+ // Lazy Mode
+ addPreference(ConfigDatabase.COLUMN_CONF_LAZY_MODE, "Lazy mode", R.string.pref_help_lazy, true);
+ // Raw Mode
+ addPreference(ConfigDatabase.COLUMN_CONF_RAW_MODE, "Raw Mode", R.string.pref_help_raw, false);
+ // Default Route
+ addPreference(ConfigDatabase.COLUMN_CONF_DEFAULT_ROUTE, "Default Route", R.string.pref_help_default_route, true);
+ }
+
+ @Override
+ protected void onDestroy() {
+ mConfigDatabase.close();
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.pref, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ protected void onStop() {
+ mConfigDatabase.insertOrUpdate(mIodineConfiguration.getContentValues());
+ Intent intent = new Intent(ACTION_CONFIGURATION_CHANGED);
+ intent.putExtra(EXTRA_CONFIGURATION_ID, mIodineConfiguration.getId());
+ sendBroadcast(intent);
+ super.onStop();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.menu_pref_delete) {
+ // Delete current connection
+ if (mIodineConfiguration.getId() != null) {
+ mConfigDatabase.delete(mIodineConfiguration.getContentValues());
+ }
+ finish();
+ } else if (item.getItemId() == android.R.id.home) {
+ finish();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/IodineVpnService.java b/src/main/java/org/xapek/andiodine/IodineVpnService.java
new file mode 100644
index 0000000..2adad1c
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/IodineVpnService.java
@@ -0,0 +1,350 @@
+package org.xapek.andiodine;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.VpnService;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import org.xapek.andiodine.config.ConfigDatabase;
+import org.xapek.andiodine.config.IodineConfiguration;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+public class IodineVpnService extends VpnService implements Runnable {
+ private class IodineVpnException extends Exception {
+ private static final long serialVersionUID = 32487871521160156L;
+
+ public IodineVpnException(String message, Throwable e) {
+ super(message, e);
+ }
+
+ public IodineVpnException(String message) {
+ super(message);
+ }
+ }
+
+ private static final String TAG = "VPN_SERVICE";
+
+ public static IodineVpnService instance = null;
+
+ /**
+ * long
+ */
+ public static final String EXTRA_CONFIGURATION_ID = "configuration_id";
+ /**
+ * String
+ */
+ public static final String EXTRA_ERROR_MESSAGE = "message";
+
+ /**
+ * Intent to connect to VPN Connection
+ * CONTROL_CONNECT(EXTRA_CONFIGURATION_ID)
+ */
+
+ public static final String ACTION_CONTROL_CONNECT = "org.xapek.andiodine.IodineVpnService.CONTROL_CONNECT";
+ /**
+ * Intent to close the vpn connection
+ */
+ public static final String ACTION_CONTROL_DISCONNECT = "org.xapek.andiodine.IodineVpnService.CONTROL_DISCONNECT";
+
+ /**
+ * Intent to request a new status update
+ */
+ public static final String ACTION_CONTROL_UPDATE = "org.xapek.andiodine.IodineVpnService.CONTROL_UPDATE";
+
+ /**
+ * Broadcast Action: The tunnel service is idle. This Action contains no
+ * extras.
+ */
+ public static final String ACTION_STATUS_IDLE = "org.xapek.andiodine.IodineVpnService.STATUS_IDLE";
+
+ /**
+ * Broadcast Action: The user sent CONTROL_CONNECT and the vpn service is
+ * trying to connect.
+ *
+ * @see #EXTRA_CONFIGURATION_ID
+ */
+ public static final String ACTION_STATUS_CONNECT = "org.xapek.andiodine.IodineVpnService.STATUS_CONNECT";
+ /**
+ * Broadcast Action: The tunnel is connected
+ *
+ * @see #EXTRA_CONFIGURATION_ID
+ */
+ public static final String ACTION_STATUS_CONNECTED = "org.xapek.andiodine.IodineVpnService.STATUS_CONNECTED";
+ /**
+ * Broadcast Action: The tunnel was disconnected
+ *
+ * @see #EXTRA_CONFIGURATION_ID
+ */
+ public static final String ACTION_STATUS_DISCONNECT = "org.xapek.andiodine.IodineVpnService.STATUS_DISCONNECT";
+ /**
+ * Broadcast Action: An error occured
+ *
+ * @see #EXTRA_CONFIGURATION_ID
+ * @see #EXTRA_ERROR_MESSAGE
+ */
+ public static final String ACTION_STATUS_ERROR = "org.xapek.andiodine.IodineVpnService.STATUS_ERROR";
+
+ private Thread mThread;
+ private ConfigDatabase configDatabase;
+ private IodineConfiguration mConfiguration;
+ private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION_CONTROL_DISCONNECT.equals(intent.getAction())) {
+ shutdown();
+ } else if (ACTION_CONTROL_UPDATE.equals(intent.getAction())) {
+ sendStatus();
+ } else if (ACTION_CONTROL_CONNECT.equals(intent.getAction())) {
+ if (mThread != null) {
+ setStatus(ACTION_STATUS_ERROR, -1L, getString(R.string.vpnservice_error_already_running));
+ return;
+ }
+
+ long configurationId = intent.getLongExtra(EXTRA_CONFIGURATION_ID, -1);
+ if (configurationId == -1L) {
+ setStatus(ACTION_STATUS_ERROR, -1L, getString(R.string.vpnservice_error_configuration_incomplete));
+ return;
+ }
+
+ mConfiguration = configDatabase.selectById(configurationId);
+ if (mConfiguration == null) {
+ setStatus(ACTION_STATUS_ERROR, mConfiguration.getId(),
+ getString(R.string.vpnservice_error_configuration_incomplete));
+ return;
+ }
+
+ mThread = new Thread(IodineVpnService.this, IodineVpnService.class.getName());
+ mThread.start();
+ }
+ }
+ };
+
+ private String currentActionStatus = ACTION_STATUS_IDLE;
+
+ private Long currentConfigurationId = null;
+
+ private String currentMessage = null;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ instance = this;
+
+ configDatabase = new ConfigDatabase(this);
+
+ IntentFilter filterInterruptAction = new IntentFilter();
+ filterInterruptAction.addAction(ACTION_CONTROL_CONNECT);
+ filterInterruptAction.addAction(ACTION_CONTROL_DISCONNECT);
+ filterInterruptAction.addAction(ACTION_CONTROL_UPDATE);
+ registerReceiver(broadcastReceiver, filterInterruptAction);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ sendStatus();
+ return START_STICKY;
+ }
+
+ private void shutdown() {
+ if (mConfiguration != null)
+ setStatus(ACTION_STATUS_DISCONNECT, mConfiguration.getId(), null);
+ else
+ setStatus(ACTION_STATUS_IDLE, null, null);
+
+ if (mThread != null) {
+ mThread.interrupt();
+ IodineClient.tunnelInterrupt();
+ }
+ }
+
+ @Override
+ public void onRevoke() {
+ shutdown();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mThread != null) {
+ mThread.interrupt();
+ IodineClient.tunnelInterrupt();
+ mThread = null;
+ }
+ instance = null;
+ configDatabase.close();
+ unregisterReceiver(broadcastReceiver);
+ }
+
+ private void setStatus(String ACTION_STATUS, Long configurationId, String message) {
+ currentActionStatus = ACTION_STATUS;
+ currentConfigurationId = configurationId;
+ currentMessage = message;
+ sendStatus();
+ }
+
+ private void sendStatus() {
+ Log.d(TAG, "Send status: " + currentActionStatus);
+ if (currentActionStatus != null) {
+ Intent intent = new Intent(currentActionStatus);
+
+ if (currentConfigurationId != null) {
+ intent.putExtra(EXTRA_CONFIGURATION_ID, currentConfigurationId);
+ }
+ if (currentMessage != null) {
+ intent.putExtra(EXTRA_ERROR_MESSAGE, currentMessage);
+ }
+ Log.d(TAG, "Send: " + intent);
+ sendBroadcast(intent);
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ Log.d(TAG, "VPN Thread enter");
+ setStatus(ACTION_STATUS_CONNECT, mConfiguration.getId(), null);
+
+ String tunnelNamesver = IodineClient.getPropertyNetDns1();
+ if (!"".equals(mConfiguration.getTunnelNameserver())) {
+ tunnelNamesver = mConfiguration.getTunnelNameserver();
+ }
+ String password = "";
+ if (!"".equals(mConfiguration.getPassword())) {
+ password = mConfiguration.getPassword();
+ }
+
+ int ret = IodineClient.connect(tunnelNamesver, mConfiguration.getTopDomain(), mConfiguration.getRawMode(),
+ mConfiguration.getLazyMode(), password);
+
+ String errorMessage = "";
+ switch (ret) {
+ case 0:
+ Log.d(TAG, "Handshake successful");
+ setStatus(ACTION_STATUS_CONNECTED, currentConfigurationId, null);
+ runTunnel(); // this blocks until connection is closed
+ setStatus(ACTION_STATUS_IDLE, null, null);
+ break;
+ case 1:
+ if (errorMessage.equals(""))
+ errorMessage = getString(R.string.vpnservice_error_cant_open_dnssocket);
+ // Fall through
+ case 2:
+ if (errorMessage.equals(""))
+ errorMessage = getString(R.string.vpnservice_error_handshake_failed);
+ // fall through
+ default:
+ if (errorMessage.equals(""))
+ errorMessage = String.format(getString(R.string.vpnservice_error_unknown_error_code), ret);
+
+ setStatus(ACTION_STATUS_ERROR, mConfiguration.getId(), errorMessage);
+ break;
+ }
+ } catch (IllegalStateException e) {
+ String errorMessage = "IllegalStateException";
+ if (e.getMessage().contains("Cannot create interface")) {
+ errorMessage = "Failed to create tunnel network device";
+ } else {
+ e.printStackTrace();
+ }
+ setStatus(ACTION_STATUS_ERROR, mConfiguration.getId(), errorMessage);
+ } catch (IodineVpnException e) {
+ e.printStackTrace();
+
+ setStatus(ACTION_STATUS_ERROR, mConfiguration.getId(),
+ String.format(getString(R.string.vpnservice_error_unknown_error_string), e.getMessage()));
+ } finally {
+ mThread = null;
+ mConfiguration = null;
+ Log.d(TAG, "VPN Thread exit");
+ }
+ }
+
+ private void runTunnel() throws IodineVpnException {
+ Builder b = new Builder();
+ b.setSession("Iodine VPN Service");
+
+ String ip = IodineClient.getIp();
+ int netbits = IodineClient.getNetbits();
+ int mtu = IodineClient.getMtu();
+ Log.d(TAG, "Build tunnel for configuration: ip=" + ip + " netbits=" + netbits + " mtu=" + mtu);
+
+ String[] ipBytesString = ip.split("\\.");
+ if (ipBytesString.length != 4) {
+ throw new IodineVpnException("Server sent invalid IP");
+ }
+ byte[] ipBytes = new byte[4];
+ for (int i = 0; i < 4; i++) {
+ try {
+ int integer = Integer.parseInt(ipBytesString[i]);
+ ipBytes[i] = (byte) (integer);
+ } catch (NumberFormatException e) {
+ throw new IodineVpnException("Server sent invalid IP", e);
+ }
+ }
+
+ InetAddress hostAddress;
+ try {
+ hostAddress = InetAddress.getByAddress(ipBytes);
+ } catch (UnknownHostException e) {
+ throw new IodineVpnException("Server sent invalid IP", e);
+ }
+ try {
+ switch (mConfiguration.getNameserverMode()) {
+ case LEAVE_DEFAULT:
+ // do nothing
+ break;
+ case SET_SERVER_TUNNEL_IP:
+ b.addDnsServer(IodineClient.getRemoteIp());
+ break;
+ case SET_CUSTOM:
+ b.addDnsServer(InetAddress.getByName(mConfiguration.getNameserver()));
+ break;
+ default:
+ throw new IodineVpnException("Invalid Nameserver mode");
+ }
+ } catch (UnknownHostException e) {
+ throw new IodineVpnException("Invalid Nameserver address", e);
+ }
+
+ b.addAddress(hostAddress, netbits);
+ if (mConfiguration.getDefaultRoute()) {
+ Log.d(TAG, "Set default route");
+ b.addRoute("0.0.0.0", 0); // Default Route
+ }
+ b.setMtu(mtu);
+
+ Log.d(TAG, "Build tunnel interface");
+ ParcelFileDescriptor parcelFD;
+ try {
+ parcelFD = b.establish();
+ } catch (Exception e) {
+ if (e.getMessage().contains("fwmark") || e.getMessage().contains("iptables")) {
+ throw new IodineVpnException(
+ "Error while creating interface, please check issue #9 at https://github.com/yvesf/andiodine/issues/9");
+ } else {
+ throw new IodineVpnException("Error while creating interface: "
+ + e.getMessage());
+ }
+ }
+
+ protect(IodineClient.getDnsFd());
+
+ int tun_fd = parcelFD.detachFd();
+
+ setStatus(ACTION_STATUS_CONNECTED, mConfiguration.getId(), null);
+
+ Log.d(TAG, "Tunnel active");
+ IodineClient.tunnel(tun_fd);
+ try {
+ ParcelFileDescriptor.adoptFd(tun_fd).close();
+ } catch (IOException e) {
+ throw new IodineVpnException(
+ "Failed to close fd after tunnel exited");
+ }
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/config/ConfigDatabase.java b/src/main/java/org/xapek/andiodine/config/ConfigDatabase.java
new file mode 100644
index 0000000..c4245ba
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/config/ConfigDatabase.java
@@ -0,0 +1,124 @@
+package org.xapek.andiodine.config;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ConfigDatabase extends SQLiteOpenHelper {
+ public static final String TAG = "ConfigDatabase";
+
+ private static final String DATABASE_NAME = "andiodine.db";
+ private static final int DATABASE_VERSION = 1;
+ static public final String TABLE_NAME_CONF = "configuration";
+ static public final String COLUMN_CONF_ID = "id";
+ static public final String COLUMN_CONF_NAME = "name";
+ static public final String COLUMN_CONF_LAST_USED = "last_used";
+ static public final String COLUMN_CONF_TUNNEL_NAMESERVER = "tunnel_nameserver";
+ static public final String COLUMN_CONF_TOP_DOMAIN = "top_domain";
+ static public final String COLUMN_CONF_PASSWORD = "password";
+ static public final String COLUMN_CONF_NAMESERVER_MODE = "nameserver_mode";
+ static public final String COLUMN_CONF_NAMESERVER = "nameserver";
+ static public final String COLUMN_CONF_RAW_MODE = "raw_mode";
+ static public final String COLUMN_CONF_LAZY_MODE = "lazy_mode";
+ static public final String COLUMN_CONF_DEFAULT_ROUTE = "default_route";
+ static public final String COLUMN_CONF_REQUEST_TYPE = "request_type";
+
+ private static final String createStmt = "CREATE TABLE " + TABLE_NAME_CONF + " (" + //
+ COLUMN_CONF_ID + " INTEGER PRIMARY KEY," + //
+ COLUMN_CONF_NAME + " TEXT," + //
+ COLUMN_CONF_LAST_USED + " INTEGER," + //
+ COLUMN_CONF_TUNNEL_NAMESERVER + " TEXT," + //
+ COLUMN_CONF_TOP_DOMAIN + " TEXT," + //
+ COLUMN_CONF_PASSWORD + " TEXT," + //
+ COLUMN_CONF_NAMESERVER_MODE + " TEXT," + //
+ COLUMN_CONF_NAMESERVER + " TEXT," + //
+ COLUMN_CONF_RAW_MODE + " INTEGER," + // Boolean stored as 1=true / 0=false
+ COLUMN_CONF_LAZY_MODE + " INTEGER," + //
+ COLUMN_CONF_DEFAULT_ROUTE + " INTEGER," + //
+ COLUMN_CONF_REQUEST_TYPE + " TEXT" + //
+ ");";
+
+ public ConfigDatabase(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(createStmt);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ }
+
+ public void insert(ContentValues config) throws SQLException {
+ if (config.getAsLong(COLUMN_CONF_ID) != null)
+ throw new SQLException("id must be null for update");
+ SQLiteDatabase writableDatabase = getWritableDatabase();
+ long id = writableDatabase.insertOrThrow(TABLE_NAME_CONF, null, config);
+ writableDatabase.close();
+ config.put(COLUMN_CONF_ID, id);
+ Log.d(TAG, "Insert id=" + id);
+ }
+
+ public int update(ContentValues config) throws SQLException {
+ if (config.getAsLong(COLUMN_CONF_ID) == null)
+ throw new SQLException("id must NOT be null for update");
+ SQLiteDatabase writableDatabase = getWritableDatabase();
+ int rows = writableDatabase.update(TABLE_NAME_CONF, config, COLUMN_CONF_ID + " = ?",
+ new String[]{config.getAsString(COLUMN_CONF_ID)});
+ writableDatabase.close();
+ Log.d(TAG, "Update rows=" + rows);
+ return rows;
+ }
+
+ public void delete(ContentValues config) throws SQLException {
+ if (config.getAsLong(COLUMN_CONF_ID) == null)
+ throw new SQLException("id must NOT be null for delete");
+ SQLiteDatabase writableDatabase = getWritableDatabase();
+ writableDatabase.delete(TABLE_NAME_CONF, COLUMN_CONF_ID + " = ?",
+ new String[]{config.getAsString(COLUMN_CONF_ID)});
+ writableDatabase.close();
+ }
+
+ public IodineConfiguration selectById(Long id) throws SQLException {
+ ContentValues v = new ContentValues();
+ SQLiteDatabase readableDatabase = getReadableDatabase();
+ Cursor query = readableDatabase.query(TABLE_NAME_CONF, null, COLUMN_CONF_ID + " = ?",
+ new String[]{id.toString()}, null, null, null);
+ query.moveToFirst();
+ DatabaseUtils.cursorRowToContentValues(query, v);
+ IodineConfiguration iodineConfiguration = new IodineConfiguration(v);
+ Log.d(TAG, "Selected: " + iodineConfiguration);
+ return iodineConfiguration;
+ }
+
+ public List<IodineConfiguration> selectAll() throws SQLException {
+ List<IodineConfiguration> configurations = new ArrayList<IodineConfiguration>();
+ SQLiteDatabase readableDatabase = getReadableDatabase();
+ Cursor query = readableDatabase.query(TABLE_NAME_CONF, null, null, null, null, null, null);
+
+ while (query.moveToNext()) {
+ ContentValues v = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(query, v);
+ configurations.add(new IodineConfiguration(v));
+ }
+ return configurations;
+ }
+
+ public void insertOrUpdate(ContentValues config) {
+ try {
+ update(config);
+ } catch (SQLException e) {
+ insert(config);
+ }
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/config/IodineConfiguration.java b/src/main/java/org/xapek/andiodine/config/IodineConfiguration.java
new file mode 100644
index 0000000..21aac6c
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/config/IodineConfiguration.java
@@ -0,0 +1,132 @@
+package org.xapek.andiodine.config;
+
+import android.content.ContentValues;
+
+/**
+ * Wrapper around ContentValues in Database
+ */
+public class IodineConfiguration {
+ public static enum NameserverMode {
+ LEAVE_DEFAULT, SET_SERVER_TUNNEL_IP, SET_CUSTOM
+ }
+
+ public static enum RequestType {
+ AUTODETECT, NULL, TXT, SRV, MX, CNAME, A
+ }
+
+ private final ContentValues v;
+
+ public IodineConfiguration() {
+ v = new ContentValues();
+ }
+
+ public IodineConfiguration(ContentValues v) {
+ this.v = v;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof IodineConfiguration) {
+ return getId() != null && getId().equals(((IodineConfiguration) o).getId());
+ } else {
+ return super.equals(o);
+ }
+ }
+
+ public Long getId() {
+ return v.getAsLong(ConfigDatabase.COLUMN_CONF_ID);
+ }
+
+ public boolean getDefaultRoute() {
+ return v.getAsInteger(ConfigDatabase.COLUMN_CONF_DEFAULT_ROUTE) == 1;
+ }
+
+ public void setDefaultRoute(boolean isDefaultRoute) {
+ v.put(ConfigDatabase.COLUMN_CONF_DEFAULT_ROUTE, isDefaultRoute ? 1 : 0);
+ }
+
+ public boolean getLazyMode() {
+ return v.getAsInteger(ConfigDatabase.COLUMN_CONF_LAZY_MODE) == 1;
+ }
+
+ public String getName() {
+ return v.getAsString(ConfigDatabase.COLUMN_CONF_NAME);
+ }
+
+ public String getNameserver() {
+ return v.getAsString(ConfigDatabase.COLUMN_CONF_NAMESERVER);
+ }
+
+ public NameserverMode getNameserverMode() {
+ return NameserverMode.valueOf(v.getAsString(ConfigDatabase.COLUMN_CONF_NAMESERVER_MODE));
+ }
+
+ public String getPassword() {
+ return v.getAsString(ConfigDatabase.COLUMN_CONF_PASSWORD);
+ }
+
+ public boolean getRawMode() {
+ return v.getAsInteger(ConfigDatabase.COLUMN_CONF_RAW_MODE) == 1;
+ }
+
+ public RequestType getRequestType() {
+ return RequestType.valueOf(v.getAsString(ConfigDatabase.COLUMN_CONF_REQUEST_TYPE));
+ }
+
+ public String getTopDomain() {
+ return v.getAsString(ConfigDatabase.COLUMN_CONF_TOP_DOMAIN);
+ }
+
+ public String getTunnelNameserver() {
+ return v.getAsString(ConfigDatabase.COLUMN_CONF_TUNNEL_NAMESERVER);
+ }
+
+ public void setLazyMode(boolean lazyMode) {
+ v.put(ConfigDatabase.COLUMN_CONF_LAZY_MODE, lazyMode ? 1 : 0);
+ }
+
+ public void setName(String name) {
+ v.put(ConfigDatabase.COLUMN_CONF_NAME, name);
+ }
+
+ public void setNameserver(String nameserver) {
+ v.put(ConfigDatabase.COLUMN_CONF_NAMESERVER, nameserver);
+ }
+
+ public void setNameserverMode(NameserverMode nameserverMode) {
+ v.put(ConfigDatabase.COLUMN_CONF_NAMESERVER_MODE, nameserverMode.name());
+ }
+
+ public void setPassword(String password) {
+ v.put(ConfigDatabase.COLUMN_CONF_PASSWORD, password);
+ }
+
+ public void setRawMode(boolean rawMode) {
+ v.put(ConfigDatabase.COLUMN_CONF_RAW_MODE, rawMode ? 1 : 0);
+ }
+
+ public void setRequestType(RequestType requestType) {
+ v.put(ConfigDatabase.COLUMN_CONF_REQUEST_TYPE, requestType.name());
+ }
+
+ public void setTopDomain(String topDomain) {
+ v.put(ConfigDatabase.COLUMN_CONF_TOP_DOMAIN, topDomain);
+ }
+
+ public void setTunnelNameserver(String tunnelNameserver) {
+ v.put(ConfigDatabase.COLUMN_CONF_TUNNEL_NAMESERVER, tunnelNameserver);
+ }
+
+ public void setId(Long id) {
+ v.put(ConfigDatabase.COLUMN_CONF_ID, id);
+ }
+
+ public ContentValues getContentValues() {
+ return v;
+ }
+
+ @Override
+ public String toString() {
+ return "[IodineConfiguration name=" + getName() + "]";
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/preferences/AbstractPreference.java b/src/main/java/org/xapek/andiodine/preferences/AbstractPreference.java
new file mode 100644
index 0000000..278de63
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/preferences/AbstractPreference.java
@@ -0,0 +1,49 @@
+package org.xapek.andiodine.preferences;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+
+abstract class AbstractPreference {
+ public static final String TAG = "Preference";
+ private final String mKey;
+ private final PreferenceActivity mPreferenceActivity;
+ private final String mTitle;
+ private final int mHelpMsgId;
+
+ public AbstractPreference(PreferenceActivity preferenceActivity, String title, int helpResId, String key) {
+ mPreferenceActivity = preferenceActivity;
+ mTitle = title;
+ mHelpMsgId = helpResId;
+ mKey = key;
+ }
+
+ protected abstract View getListItemView(Context context);
+
+ public void persist(final String value) {
+ Log.d(TAG, String.format("persist String %s -> %s", mKey, value));
+ mPreferenceActivity.getContentValues().put(mKey, value);
+ }
+
+ public void persist(final boolean value) {
+ Log.d(TAG, String.format("persist boolean %s -> %s", mKey, "" + value));
+ mPreferenceActivity.getContentValues().put(mKey, value ? 1 : 0);
+ }
+
+ public boolean getAsBoolean() {
+ return mPreferenceActivity.getContentValues().getAsInteger(mKey) != null
+ && mPreferenceActivity.getContentValues().getAsInteger(mKey) == 1;
+ }
+
+ public String getAsString() {
+ return mPreferenceActivity.getContentValues().getAsString(mKey);
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public String getMessage() {
+ return mPreferenceActivity.getResources().getString(mHelpMsgId);
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/preferences/BooleanPreference.java b/src/main/java/org/xapek/andiodine/preferences/BooleanPreference.java
new file mode 100644
index 0000000..9e7917d
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/preferences/BooleanPreference.java
@@ -0,0 +1,33 @@
+package org.xapek.andiodine.preferences;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+import org.xapek.andiodine.R;
+
+public class BooleanPreference extends AbstractPreference {
+
+ public BooleanPreference(PreferenceActivity preferenceActivity, String title, int helpMsgId, String key) {
+ super(preferenceActivity, title, helpMsgId, key);
+ }
+
+ @Override
+ protected View getListItemView(Context context) {
+ CheckBox view = new CheckBox(context);
+ view.setText(context.getString(R.string.enable) + " " + getTitle());
+ Log.d(TAG, "Status: " + getTitle() + " = " + getAsBoolean());
+ view.setChecked(getAsBoolean());
+ view.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ persist(isChecked);
+ }
+ });
+ return view;
+ }
+
+}
diff --git a/src/main/java/org/xapek/andiodine/preferences/PreferenceActivity.java b/src/main/java/org/xapek/andiodine/preferences/PreferenceActivity.java
new file mode 100644
index 0000000..dd0faa4
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/preferences/PreferenceActivity.java
@@ -0,0 +1,104 @@
+package org.xapek.andiodine.preferences;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ContentValues;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import org.xapek.andiodine.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class PreferenceActivity extends ListActivity {
+ private ArrayList<AbstractPreference> mPreferences;
+
+ private ContentValues mContentValues = new ContentValues();
+
+ private static class DialogPreferenceAdapter extends ArrayAdapter<AbstractPreference> {
+
+ private final LayoutInflater mInflater;
+
+ private static class HelpOnClickListener implements OnClickListener {
+ private final AbstractPreference p;
+
+ public HelpOnClickListener(AbstractPreference p) {
+ this.p = p;
+ }
+
+ @Override
+ public void onClick(View v) {
+ new AlertDialog.Builder(v.getContext())//
+ .setTitle(p.getTitle())//
+ .setMessage(p.getMessage())//
+ .create().show();
+ }
+ }
+
+ public DialogPreferenceAdapter(Context context, List<AbstractPreference> preferences) {
+ super(context, -1, preferences);
+ mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ AbstractPreference item = getItem(position);
+ View rowView = mInflater.inflate(R.layout.rowlayout, parent, false);
+ LinearLayout content = (LinearLayout) rowView.findViewById(R.id.rowlayout_content);
+ ImageButton helpButton = (ImageButton) rowView.findViewById(R.id.rowlayout_help);
+ TextView title = (TextView) rowView.findViewById(R.id.rowlayout_title);
+
+ content.addView(item.getListItemView(getContext()));
+ helpButton.setOnClickListener(new HelpOnClickListener(item));
+ title.setText(item.getTitle());
+
+ return rowView;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mPreferences = new ArrayList<AbstractPreference>();
+
+ setListAdapter(new DialogPreferenceAdapter(this, mPreferences));
+ }
+
+ public void setContentValues(final ContentValues contentValues) {
+ mContentValues = contentValues;
+ }
+
+ public ContentValues getContentValues() {
+ return mContentValues;
+ }
+
+ public void addPreference(String key, String title, int helpMsgId, String defaultValue) {
+ if (mContentValues.get(key) == null)
+ mContentValues.put(key, defaultValue);
+ mPreferences.add(new TextPreference(this, title, helpMsgId, key));
+ }
+
+ public void addPreference(String key, String title, int helpMsgId, boolean defaultValue) {
+ BooleanPreference preference = new BooleanPreference(this, title, helpMsgId, key);
+ if (mContentValues.get(key) == null)
+ preference.persist(defaultValue);
+ mPreferences.add(preference);
+ }
+
+ public void addPreference(String key, String title, int helpMsgId, String[] values, String defaultValue) {
+ if (mContentValues.get(key) == null)
+ mContentValues.put(key, defaultValue);
+ mPreferences.add(new SpinnerPreference(this, key, title, helpMsgId, values));
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/preferences/SpinnerPreference.java b/src/main/java/org/xapek/andiodine/preferences/SpinnerPreference.java
new file mode 100644
index 0000000..3528f05
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/preferences/SpinnerPreference.java
@@ -0,0 +1,47 @@
+package org.xapek.andiodine.preferences;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+public class SpinnerPreference extends AbstractPreference {
+ private final String[] mValues;
+
+ public SpinnerPreference(PreferenceActivity preferenceActivity, String key, String title, int helpResId,
+ String[] values) {
+ super(preferenceActivity, title, helpResId, key);
+ mValues = values;
+ }
+
+ @Override
+ protected View getListItemView(Context context) {
+ Spinner view = new Spinner(context);
+ view.setAdapter(new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, mValues));
+
+ int i = 0;
+ for (final String value : mValues) {
+ if (value != null && value.equals(getAsString())) {
+ view.setSelection(i);
+ break;
+ }
+ i++;
+ }
+
+ view.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ String item = mValues[position];
+ persist(item);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+
+ return view;
+ }
+}
diff --git a/src/main/java/org/xapek/andiodine/preferences/TextPreference.java b/src/main/java/org/xapek/andiodine/preferences/TextPreference.java
new file mode 100644
index 0000000..ae28f1e
--- /dev/null
+++ b/src/main/java/org/xapek/andiodine/preferences/TextPreference.java
@@ -0,0 +1,35 @@
+package org.xapek.andiodine.preferences;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.EditText;
+
+public class TextPreference extends AbstractPreference {
+ public TextPreference(PreferenceActivity preferenceActivity, String title, int helpMsgId, String key) {
+ super(preferenceActivity, title, helpMsgId, key);
+ }
+
+ @Override
+ protected View getListItemView(Context context) {
+ final EditText view = new EditText(context);
+ view.setSingleLine();
+ view.setText(getAsString());
+ view.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ persist(s.toString());
+ }
+ });
+ return view;
+ }
+}