From 3b4ce6d5588c87bcc621a0054166c81de31d63aa Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Wed, 20 Nov 2019 17:48:31 +0100 Subject: Implement importing profiles from Access Server --- .../blinkt/openvpn/activities/ConfigConverter.kt | 40 ++- .../de/blinkt/openvpn/fragments/ImportASConfig.kt | 327 +++++++++++++++++++++ .../blinkt/openvpn/fragments/VPNProfileList.java | 277 ++++++++--------- 3 files changed, 491 insertions(+), 153 deletions(-) create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt (limited to 'main/src/ui/java/de') diff --git a/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.kt b/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.kt index b2a76f3d..b91628a2 100644 --- a/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.kt +++ b/main/src/ui/java/de/blinkt/openvpn/activities/ConfigConverter.kt @@ -37,6 +37,7 @@ import de.blinkt.openvpn.views.FileSelectLayout import de.blinkt.openvpn.views.FileSelectLayout.FileSelectCallback import java.io.* import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.util.* class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener { @@ -511,7 +512,7 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener // Read in the bytes var offset = 0 - var bytesRead:Int + var bytesRead: Int do { bytesRead = input.read(bytes, offset, bytes.size - offset) offset += bytesRead @@ -604,11 +605,13 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener } private fun doImportIntent(intent: Intent) { - val data = intent.data - if (intent.action.equals(IMPORT_PROFILE_DATA)) - if (data != null) { - mSourceUri = data - doImportUri(data) + if (intent.action.equals(IMPORT_PROFILE_DATA)) { + val data = intent.getStringExtra(Intent.EXTRA_TEXT) + + if (data != null) { + startImportTask(Uri.fromParts("inline", "inlinetext", null), + "imported profiles from AS", data); + } } } @@ -650,12 +653,12 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener possibleName = possibleName.replace(".conf", "") } - startImportTask(data, possibleName) + startImportTask(data, possibleName, "") } - private fun startImportTask(data: Uri, possibleName: String?) { + private fun startImportTask(data: Uri, possibleName: String?, inlineData:String) { mImportTask = object : AsyncTask() { private var mProgress: ProgressBar? = null @@ -666,10 +669,16 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener override fun doInBackground(vararg params: Void): Int? { try { - val `is` = contentResolver.openInputStream(data) - - doImport(`is`) - `is`!!.close() + var inputStream:InputStream? + if (data.scheme.equals("inline")) { + inputStream = inlineData.byteInputStream() + } else { + inputStream = contentResolver.openInputStream(data) + } + + if (inputStream != null) { + doImport(inputStream) + } if (mResult == null) return -3 } catch (se: IOException) { @@ -738,10 +747,10 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener mLogLayout.addView(view, mLogLayout.childCount - 1) } - private fun doImport(`is`: InputStream?) { + private fun doImport(inputStream: InputStream) { val cp = ConfigParser() try { - val isr = InputStreamReader(`is`!!) + val isr = InputStreamReader(inputStream) cp.parseConfig(isr) mResult = cp.convertProfile() @@ -757,9 +766,10 @@ class ConfigConverter : BaseActivity(), FileSelectCallback, View.OnClickListener } mResult = null - + inputStream.close() } + private fun displayWarnings() { if (mResult!!.mUseCustomConfig) { log(R.string.import_warning_custom_options) diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt new file mode 100644 index 00000000..f501c0cf --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2012-2019 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.TrafficStats +import android.os.Bundle +import android.util.Base64 +import android.util.Base64.NO_WRAP +import android.util.Log +import android.widget.CheckBox +import android.widget.EditText +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import de.blinkt.openvpn.R +import de.blinkt.openvpn.activities.ConfigConverter +import de.blinkt.openvpn.core.Preferences +import okhttp3.* +import okhttp3.internal.tls.OkHostnameVerifier +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.runOnUiThread +import java.io.IOException +import java.lang.Exception +import java.security.MessageDigest +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.* + +class BasicAuthInterceptor(user: String, password: String) : Interceptor { + + private val credentials: String + + init { + this.credentials = Credentials.basic(user, password) + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val authenticatedRequest = request.newBuilder() + .header("Authorization", credentials).build() + return chain.proceed(authenticatedRequest) + } + +} + + +fun getCompositeSSLSocketFactory(certPin: CertificatePinner, hostname: String): SSLSocketFactory { + val trustPinnedCerts = arrayOf(object : X509TrustManager { + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + throw CertificateException("Why would we check client certificates?!") + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + certPin.check(hostname, chain.toList()) + } + }) + + // Install the all-trusting trust manager + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustPinnedCerts, java.security.SecureRandom()) + // Create an ssl socket factory with our all-trusting manager + + return sslContext.socketFactory + +} + +class ImportASConfig : DialogFragment() { + private lateinit var asUseAutlogin: CheckBox + private lateinit var asServername: EditText + private lateinit var asUsername: EditText + private lateinit var asPassword: EditText + + + internal fun getHostNameVerifier(prefs: SharedPreferences): HostnameVerifier { + val pinnedHostnames: Set = prefs.getStringSet("pinnedHosts", emptySet())!! + + val mapping = mutableMapOf() + + pinnedHostnames.forEach { ph -> + mapping[ph] = prefs.getString("pin-${ph}", "") + } + + val defaultVerifier = OkHostnameVerifier.INSTANCE; + val pinHostVerifier = object : HostnameVerifier { + override fun verify(hostname: String?, session: SSLSession?): Boolean { + val unverifiedHandshake = Handshake.get(session) + val cert = unverifiedHandshake.peerCertificates()[0] as X509Certificate + val hostPin = CertificatePinner.pin(cert) + + if (mapping.containsKey(hostname) && mapping[hostname] == hostPin) + return true + else + return defaultVerifier.verify(hostname, session) + } + + } + return pinHostVerifier + } + + internal fun buildHttpClient(c: Context, user: String, password: String, hostname: String): OkHttpClient { + + // TODO: HACK + val THREAD_ID = 10000; + TrafficStats.setThreadStatsTag(THREAD_ID); + + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pinnedHosts: Set = prefs.getStringSet("pinnedHosts", emptySet())!! + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(BasicAuthInterceptor(user, password)) + + /* Rely on system certificates if we do not have the host pinned */ + if (pinnedHosts.contains(hostname)) { + val cpb = CertificatePinner.Builder() + + pinnedHosts.forEach { ph -> + cpb.add(ph, prefs.getString("pin-${ph}", "")) + } + + + val certPinner = cpb.build() + getCompositeSSLSocketFactory(certPinner, hostname).let { + okHttpClient.sslSocketFactory(it) + } + //okHttpClient.certificatePinner(certPinner) + } + + okHttpClient.hostnameVerifier(getHostNameVerifier(prefs)) + + val client = okHttpClient.build() + return client + + } + + /** + * @param fp Fingerprint in sha 256 format + */ + internal fun addPinnedCert(c: Context, host: String, fp: String) { + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pedit = prefs.edit() + val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!! + + pinnedHosts.add(host) + + pedit.putString("pin-${host}", "sha256/${fp}") + + pedit.putStringSet("pinnedHosts", pinnedHosts) + + pedit.apply() + } + + fun fetchProfile(c: Context, asUri: HttpUrl, user: String, password: String): Response? { + + + val httpClient = buildHttpClient(c, user, password, asUri.host() ?: "") + + val request = Request.Builder() + .url(asUri) + .build() + + val response = httpClient.newCall(request).execute() + + return response + + } + + private fun getAsUrl(url: String, autologin: Boolean): HttpUrl { + var asurl = url + if (!asurl.startsWith("http")) + asurl = "https://" + asurl + + if (autologin) + asurl += "/rest/GetAutologin" + else + asurl += "/rest/GetUserlogin" + + val asUri = HttpUrl.parse(asurl) + return asUri + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val inflater = requireActivity().layoutInflater + val view = inflater.inflate(R.layout.import_as_config, null); + + val builder = AlertDialog.Builder(requireContext()) + + builder.setView(view) + + + builder.setTitle(R.string.import_from_as) + + asServername = view.findViewById(R.id.as_servername) + asUsername = view.findViewById(R.id.username) + asPassword = view.findViewById(R.id.password) + asUseAutlogin = view.findViewById(R.id.request_autologin) + + builder.setPositiveButton(R.string.import_config, null) + builder.setNegativeButton(android.R.string.cancel) { _, _ -> } + + val dialog = builder.create() + + dialog.setOnShowListener() { d2 -> + val d: AlertDialog = d2 as AlertDialog + + d.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener() + { _ -> + doAsImport() + } + } + return dialog + } + + internal fun doAsImport() { + val ab = AlertDialog.Builder(requireContext()) + ab.setTitle("Downloading profile") + ab.setMessage("Please wait") + val pleaseWait = ab.show() + Toast.makeText(context, "Downloading profile", Toast.LENGTH_LONG).show() + val asProfileUri = getAsUrl(asServername.text.toString(), asUseAutlogin.isChecked) + + doAsync { + var e: Exception? = null + try { + val response = fetchProfile(requireContext(), asProfileUri, + asUsername.text.toString(), asPassword.text.toString()) + + if (response == null) { + throw Exception("No Response from Server") + } else if (response.isSuccessful) { + val profile = response.body().string() + activity?.runOnUiThread() { + pleaseWait?.dismiss() + val startImport = Intent(activity, ConfigConverter::class.java) + startImport.action = ConfigConverter.IMPORT_PROFILE_DATA + startImport.putExtra(Intent.EXTRA_TEXT, profile) + startActivity(startImport) + dismiss() + } + } else { + throw Exception("Invalid Response from server: \n${response.code()} ${response.message()} \n\n ${response.body().string()}") + } + + } catch (ce: SSLHandshakeException) { + // Find out if we are in the non trust path + if (ce.cause is CertificateException && ce.cause != null) { + val certExp: CertificateException = (ce.cause as CertificateException) + if (certExp.cause is CertPathValidatorException && certExp.cause != null) { + val caPathExp: CertPathValidatorException = certExp.cause as CertPathValidatorException + if (caPathExp.certPath.type.equals("X.509") && caPathExp.certPath.certificates.size > 0) { + val firstCert: X509Certificate = (caPathExp.certPath.certificates[0] as X509Certificate) + + val fpBytes = MessageDigest.getInstance("SHA-256").digest(firstCert.publicKey.encoded) + val fp = Base64.encodeToString(fpBytes, NO_WRAP) + + + + Log.i("OpenVPN", "Found cert with FP ${fp}: ${firstCert.subjectDN}") + requireContext().runOnUiThread { + + pleaseWait?.dismiss() + + AlertDialog.Builder(requireContext()) + .setTitle("Untrusted certificate found") + .setMessage(firstCert.toString()) + .setPositiveButton("Trust") { _, _ -> addPinnedCert(requireContext(), asProfileUri.host(), fp) } + .setNegativeButton("Do not trust", null) + .show() + Toast.makeText(requireContext(), "Found cert with FP ${fp}: ${firstCert.subjectDN.toString()}", Toast.LENGTH_LONG).show() + } + } + } + } else { + e = ce + } + } catch (ge: Exception) { + e = ge + } + if (e != null) { + activity?.runOnUiThread() { + pleaseWait?.dismiss() + AlertDialog.Builder(requireContext()) + .setTitle("Import failed") + .setMessage("Error: " + e.localizedMessage) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + } + } + + + override fun onResume() { + super.onResume() + asServername.setText(Preferences.getDefaultSharedPreferences(activity).getString("as-hostname", "")) + asUsername.setText(Preferences.getDefaultSharedPreferences(activity).getString("as-username", "")) + } + + override fun onPause() { + super.onPause() + val prefs = Preferences.getDefaultSharedPreferences(activity) + prefs.edit().putString("as-hostname", asServername.text.toString()).apply() + prefs.edit().putString("as-username", asUsername.text.toString()).apply() + } + + companion object { + @JvmStatic + fun newInstance(): ImportASConfig { + return ImportASConfig(); + } + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java index eaccd201..eb81e62e 100644 --- a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2016 Arne Schwabe + * Copyright (c) 2012-2019 Arne Schwabe * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt */ @@ -19,7 +19,9 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.PersistableBundle; + import androidx.annotation.RequiresApi; +import androidx.fragment.app.DialogFragment; import androidx.fragment.app.ListFragment; import android.text.Html; @@ -62,22 +64,24 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn public final static int RESULT_VPN_DELETED = Activity.RESULT_FIRST_USER; public final static int RESULT_VPN_DUPLICATE = Activity.RESULT_FIRST_USER + 1; - + // Shortcut version is increased to refresh all shortcuts + final static int SHORTCUT_VERSION = 1; private static final int MENU_ADD_PROFILE = Menu.FIRST; - private static final int START_VPN_CONFIG = 92; private static final int SELECT_PROFILE = 43; private static final int IMPORT_PROFILE = 231; private static final int FILE_PICKER_RESULT_KITKAT = 392; - private static final int MENU_IMPORT_PROFILE = Menu.FIRST + 1; private static final int MENU_CHANGE_SORTING = Menu.FIRST + 2; + private static final int MENU_IMPORT_AS = Menu.FIRST + 3; private static final String PREF_SORT_BY_LRU = "sortProfilesByLRU"; + protected VpnProfile mEditProfile = null; private String mLastStatusMessage; + private ArrayAdapter mArrayadapter; @Override public void updateState(String state, String logmessage, final int localizedResId, ConnectionStatus level) { - getActivity().runOnUiThread(() -> { + requireActivity().runOnUiThread(() -> { mLastStatusMessage = VpnStatus.getLastCleanLogMessage(getActivity()); mArrayadapter.notifyDataSetChanged(); }); @@ -87,51 +91,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn public void setConnectedVPN(String uuid) { } - private class VPNArrayAdapter extends ArrayAdapter { - - public VPNArrayAdapter(Context context, int resource, - int textViewResourceId) { - super(context, resource, textViewResourceId); - } - - @Override - public View getView(final int position, View convertView, ViewGroup parent) { - View v = super.getView(position, convertView, parent); - - final VpnProfile profile = (VpnProfile) getListAdapter().getItem(position); - - View titleview = v.findViewById(R.id.vpn_list_item_left); - titleview.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - startOrStopVPN(profile); - } - }); - - View settingsview = v.findViewById(R.id.quickedit_settings); - settingsview.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - editVPN(profile); - - } - }); - - TextView subtitle = (TextView) v.findViewById(R.id.vpn_item_subtitle); - if (profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) { - subtitle.setText(mLastStatusMessage); - subtitle.setVisibility(View.VISIBLE); - } else { - subtitle.setText(""); - subtitle.setVisibility(View.GONE); - } - - - return v; - } - } - private void startOrStopVPN(VpnProfile profile) { if (VpnStatus.isVPNActive() && profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) { Intent disconnectVPN = new Intent(getActivity(), DisconnectVPN.class); @@ -141,22 +100,12 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn } } - - private ArrayAdapter mArrayadapter; - - protected VpnProfile mEditProfile = null; - - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } - - // Shortcut version is increased to refresh all shortcuts - final static int SHORTCUT_VERSION = 1; - @RequiresApi(api = Build.VERSION_CODES.N_MR1) void updateDynamicShortcuts() { PersistableBundle versionExtras = new PersistableBundle(); @@ -267,28 +216,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn .build(); } - class MiniImageGetter implements ImageGetter { - - - @Override - public Drawable getDrawable(String source) { - Drawable d = null; - if ("ic_menu_add".equals(source)) - d = getActivity().getResources().getDrawable(R.drawable.ic_menu_add_grey); - else if ("ic_menu_archive".equals(source)) - d = getActivity().getResources().getDrawable(R.drawable.ic_menu_import_grey); - - - if (d != null) { - d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); - return d; - } else { - return null; - } - } - } - - @Override public void onResume() { super.onResume(); @@ -334,55 +261,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn setListAdapter(); } - static class VpnProfileNameComparator implements Comparator { - - @Override - public int compare(VpnProfile lhs, VpnProfile rhs) { - if (lhs == rhs) - // Catches also both null - return 0; - - if (lhs == null) - return -1; - if (rhs == null) - return 1; - - if (lhs.mName == null) - return -1; - if (rhs.mName == null) - return 1; - - return lhs.mName.compareTo(rhs.mName); - } - - } - - static class VpnProfileLRUComparator implements Comparator { - - VpnProfileNameComparator nameComparator = new VpnProfileNameComparator(); - - @Override - public int compare(VpnProfile lhs, VpnProfile rhs) { - if (lhs == rhs) - // Catches also both null - return 0; - - if (lhs == null) - return -1; - if (rhs == null) - return 1; - - // Copied from Long.compare - if (lhs.mLastUsed > rhs.mLastUsed) - return -1; - if (lhs.mLastUsed < rhs.mLastUsed) - return 1; - else - return nameComparator.compare(lhs, rhs); - } - } - - private void setListAdapter() { if (mArrayadapter == null) { mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title); @@ -408,7 +286,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn mArrayadapter.notifyDataSetChanged(); } - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.add(0, MENU_ADD_PROFILE, 0, R.string.menu_add_profile) @@ -429,8 +306,13 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn .setTitleCondensed(getString(R.string.sort)) .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM); - } + menu.add(0, MENU_IMPORT_AS, 0, R.string.import_from_as) + .setIcon(R.drawable.ic_menu_import) + .setAlphabeticShortcut('p') + .setTitleCondensed("Import AS") + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM); + } @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -442,13 +324,21 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn return startImportConfigFilePicker(); } else if (itemId == MENU_CHANGE_SORTING) { return changeSorting(); + } else if (itemId == MENU_IMPORT_AS) { + return startASProfileImport(); } else { return super.onOptionsItemSelected(item); } } + private boolean startASProfileImport() { + ImportASConfig asImportFrag = ImportASConfig.newInstance(); + asImportFrag.show(requireFragmentManager(), "dialog"); + return true; + } + private boolean changeSorting() { - SharedPreferences prefs = Preferences.getDefaultSharedPreferences(getActivity()); + SharedPreferences prefs = Preferences.getDefaultSharedPreferences(requireActivity()); boolean oldValue = prefs.getBoolean(PREF_SORT_BY_LRU, false); SharedPreferences.Editor prefsedit = prefs.edit(); if (oldValue) { @@ -477,7 +367,7 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn private boolean startImportConfigFilePicker() { boolean startOldFileDialog = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utils.alwaysUseOldFileChooser(getActivity() )) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utils.alwaysUseOldFileChooser(getActivity())) startOldFileDialog = !startFilePicker(); if (startOldFileDialog) @@ -504,7 +394,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn startActivityForResult(intent, SELECT_PROFILE); } - private void onAddOrDuplicateProfile(final VpnProfile mCopyProfile) { Context context = getActivity(); if (context != null) { @@ -560,7 +449,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn return ProfileManager.getInstance(getActivity()); } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -612,7 +500,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn startActivityForResult(startImport, IMPORT_PROFILE); } - private void editVPN(VpnProfile profile) { mEditProfile = profile; Intent vprefintent = new Intent(getActivity(), VPNPreferences.class) @@ -630,4 +517,118 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn intent.setAction(Intent.ACTION_MAIN); startActivity(intent); } + + static class VpnProfileNameComparator implements Comparator { + + @Override + public int compare(VpnProfile lhs, VpnProfile rhs) { + if (lhs == rhs) + // Catches also both null + return 0; + + if (lhs == null) + return -1; + if (rhs == null) + return 1; + + if (lhs.mName == null) + return -1; + if (rhs.mName == null) + return 1; + + return lhs.mName.compareTo(rhs.mName); + } + + } + + static class VpnProfileLRUComparator implements Comparator { + + VpnProfileNameComparator nameComparator = new VpnProfileNameComparator(); + + @Override + public int compare(VpnProfile lhs, VpnProfile rhs) { + if (lhs == rhs) + // Catches also both null + return 0; + + if (lhs == null) + return -1; + if (rhs == null) + return 1; + + // Copied from Long.compare + if (lhs.mLastUsed > rhs.mLastUsed) + return -1; + if (lhs.mLastUsed < rhs.mLastUsed) + return 1; + else + return nameComparator.compare(lhs, rhs); + } + } + + private class VPNArrayAdapter extends ArrayAdapter { + + public VPNArrayAdapter(Context context, int resource, + int textViewResourceId) { + super(context, resource, textViewResourceId); + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + View v = super.getView(position, convertView, parent); + + final VpnProfile profile = (VpnProfile) getListAdapter().getItem(position); + + View titleview = v.findViewById(R.id.vpn_list_item_left); + titleview.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + startOrStopVPN(profile); + } + }); + + View settingsview = v.findViewById(R.id.quickedit_settings); + settingsview.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + editVPN(profile); + + } + }); + + TextView subtitle = (TextView) v.findViewById(R.id.vpn_item_subtitle); + if (profile.getUUIDString().equals(VpnStatus.getLastConnectedVPNProfile())) { + subtitle.setText(mLastStatusMessage); + subtitle.setVisibility(View.VISIBLE); + } else { + subtitle.setText(""); + subtitle.setVisibility(View.GONE); + } + + + return v; + } + } + + class MiniImageGetter implements ImageGetter { + + + @Override + public Drawable getDrawable(String source) { + Drawable d = null; + if ("ic_menu_add".equals(source)) + d = getActivity().getResources().getDrawable(R.drawable.ic_menu_add_grey); + else if ("ic_menu_archive".equals(source)) + d = getActivity().getResources().getDrawable(R.drawable.ic_menu_import_grey); + + + if (d != null) { + d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); + return d; + } else { + return null; + } + } + } } -- cgit v1.2.3