diff options
Diffstat (limited to 'src/org/xapek/andiodine')
-rw-r--r-- | src/org/xapek/andiodine/FragmentList.java | 207 | ||||
-rw-r--r-- | src/org/xapek/andiodine/FragmentStatus.java | 107 | ||||
-rw-r--r-- | src/org/xapek/andiodine/IodineClient.java | 54 | ||||
-rw-r--r-- | src/org/xapek/andiodine/IodineMain.java | 102 | ||||
-rw-r--r-- | src/org/xapek/andiodine/IodinePref.java | 112 | ||||
-rw-r--r-- | src/org/xapek/andiodine/IodineVpnService.java | 328 | ||||
-rw-r--r-- | src/org/xapek/andiodine/config/ConfigDatabase.java | 126 | ||||
-rw-r--r-- | src/org/xapek/andiodine/config/IodineConfiguration.java | 127 | ||||
-rw-r--r-- | src/org/xapek/andiodine/preferences/AbstractPreference.java | 49 | ||||
-rw-r--r-- | src/org/xapek/andiodine/preferences/BooleanPreference.java | 33 | ||||
-rw-r--r-- | src/org/xapek/andiodine/preferences/PreferenceActivity.java | 104 | ||||
-rw-r--r-- | src/org/xapek/andiodine/preferences/SpinnerPreference.java | 47 | ||||
-rw-r--r-- | src/org/xapek/andiodine/preferences/TextPreference.java | 35 |
13 files changed, 1431 insertions, 0 deletions
diff --git a/src/org/xapek/andiodine/FragmentList.java b/src/org/xapek/andiodine/FragmentList.java new file mode 100644 index 0000000..e07aee4 --- /dev/null +++ b/src/org/xapek/andiodine/FragmentList.java @@ -0,0 +1,207 @@ +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.ImageButton; +import android.widget.LinearLayout; +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/org/xapek/andiodine/FragmentStatus.java b/src/org/xapek/andiodine/FragmentStatus.java new file mode 100644 index 0000000..aaa64fb --- /dev/null +++ b/src/org/xapek/andiodine/FragmentStatus.java @@ -0,0 +1,107 @@ +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.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 mCancel; + + 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())) { + new AlertDialog.Builder(FragmentStatus.this.getActivity())// + .setIcon(R.drawable.error) // + .setTitle(intent.getStringExtra(IodineVpnService.EXTRA_ERROR_MESSAGE)) // + .setMessage(mLogmessages.getText()) // + .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"); + } 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())) { + mLogmessages.append("\n"); + mLogmessages.append(intent.getStringExtra(IodineClient.EXTRA_MESSAGE)); + 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); + mCancel = (Button) getActivity().findViewById(R.id.status_cancel); + mCancel.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/org/xapek/andiodine/IodineClient.java b/src/org/xapek/andiodine/IodineClient.java new file mode 100644 index 0000000..e9383ec --- /dev/null +++ b/src/org/xapek/andiodine/IodineClient.java @@ -0,0 +1,54 @@ +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) { + Log.d(TAG, "Message: " + 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/org/xapek/andiodine/IodineMain.java b/src/org/xapek/andiodine/IodineMain.java new file mode 100644 index 0000000..b53ea10 --- /dev/null +++ b/src/org/xapek/andiodine/IodineMain.java @@ -0,0 +1,102 @@ +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())) { + // 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); + + IntentFilter intentFilterStatusUpdates = new IntentFilter(); + intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_CONNECT); + intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_ERROR); + intentFilterStatusUpdates.addAction(IodineVpnService.ACTION_STATUS_IDLE); + registerReceiver(broadcastReceiverStatusUpdates, intentFilterStatusUpdates); + + startService(new Intent(this, IodineVpnService.class)); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + protected void onDestroy() { + unregisterReceiver(broadcastReceiverStatusUpdates); + 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(); + } else if (item.getItemId() == R.id.menu_main_add) { + startActivity(new Intent(this, IodinePref.class)); + } + return super.onOptionsItemSelected(item); + } + + + private void vpnServiceDisconnect() { + sendBroadcast(new Intent(IodineVpnService.ACTION_CONTROL_DISCONNECT)); + } +} diff --git a/src/org/xapek/andiodine/IodinePref.java b/src/org/xapek/andiodine/IodinePref.java new file mode 100644 index 0000000..cc21b59 --- /dev/null +++ b/src/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/org/xapek/andiodine/IodineVpnService.java b/src/org/xapek/andiodine/IodineVpnService.java new file mode 100644 index 0000000..f816aaa --- /dev/null +++ b/src/org/xapek/andiodine/IodineVpnService.java @@ -0,0 +1,328 @@ +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.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() { + setStatus(ACTION_STATUS_DISCONNECT, mConfiguration.getId(), 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() { + 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(); + 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 = b.establish(); + + protect(IodineClient.getDnsFd()); + + int tun_fd = parcelFD.detachFd(); + + setStatus(ACTION_STATUS_CONNECTED, mConfiguration.getId(), null); + + Log.d(TAG, "Tunnel active"); + IodineClient.tunnel(tun_fd); + } +} diff --git a/src/org/xapek/andiodine/config/ConfigDatabase.java b/src/org/xapek/andiodine/config/ConfigDatabase.java new file mode 100644 index 0000000..ffc1656 --- /dev/null +++ b/src/org/xapek/andiodine/config/ConfigDatabase.java @@ -0,0 +1,126 @@ +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, config.toString()); + 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); + assert query.isLast(); + query.moveToFirst(); + DatabaseUtils.cursorRowToContentValues(query, v); + Log.d(TAG, "Selected: " + v.toString()); + return new IodineConfiguration(v); + } + + 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/org/xapek/andiodine/config/IodineConfiguration.java b/src/org/xapek/andiodine/config/IodineConfiguration.java new file mode 100644 index 0000000..c41621b --- /dev/null +++ b/src/org/xapek/andiodine/config/IodineConfiguration.java @@ -0,0 +1,127 @@ +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; + } +} diff --git a/src/org/xapek/andiodine/preferences/AbstractPreference.java b/src/org/xapek/andiodine/preferences/AbstractPreference.java new file mode 100644 index 0000000..278de63 --- /dev/null +++ b/src/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/org/xapek/andiodine/preferences/BooleanPreference.java b/src/org/xapek/andiodine/preferences/BooleanPreference.java new file mode 100644 index 0000000..9e7917d --- /dev/null +++ b/src/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/org/xapek/andiodine/preferences/PreferenceActivity.java b/src/org/xapek/andiodine/preferences/PreferenceActivity.java new file mode 100644 index 0000000..dd0faa4 --- /dev/null +++ b/src/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/org/xapek/andiodine/preferences/SpinnerPreference.java b/src/org/xapek/andiodine/preferences/SpinnerPreference.java new file mode 100644 index 0000000..3528f05 --- /dev/null +++ b/src/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/org/xapek/andiodine/preferences/TextPreference.java b/src/org/xapek/andiodine/preferences/TextPreference.java new file mode 100644 index 0000000..ae28f1e --- /dev/null +++ b/src/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; + } +} |